@rt-tools/agent-kit 0.5.2 → 0.6.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.
Files changed (52) hide show
  1. package/README.md +34 -6
  2. package/assets/checks/check-file-size.mjs +127 -0
  3. package/assets/checks/rt-kit-checks.config.mjs +10 -0
  4. package/assets/defaults/gate-map.sh +90 -34
  5. package/assets/defaults/project.sh +26 -3
  6. package/assets/hooks/skill-gate-layers.sh +156 -0
  7. package/assets/hooks/skill-gate.sh +11 -2
  8. package/assets/laws/code-structure.md +3 -0
  9. package/assets/laws/delivery.md +9 -0
  10. package/assets/laws/observability.md +46 -0
  11. package/assets/laws/project-documentation.md +4 -0
  12. package/assets/laws/reuse-first.md +2 -0
  13. package/assets/laws/verifiability.md +5 -0
  14. package/assets/patterns/browser-verification-stand.md +22 -2
  15. package/assets/patterns/doc-style-trace.md +111 -0
  16. package/assets/patterns/git-workflow-commit.github.md +1 -1
  17. package/assets/patterns/git-workflow-docker.md +203 -0
  18. package/assets/patterns/git-workflow-secrets.md +93 -0
  19. package/assets/patterns/observability-record.md +114 -0
  20. package/assets/patterns/ownership-session-procedure.md +102 -0
  21. package/assets/patterns/seo-verify.md +1 -1
  22. package/assets/patterns/spec-driven-rule.md +5 -0
  23. package/assets/patterns/styling-bem-sheet.md +178 -0
  24. package/assets/patterns/task-flow-close.md +20 -0
  25. package/assets/patterns/task-flow-resume.md +5 -0
  26. package/assets/patterns/translations-content.md +107 -0
  27. package/assets/patterns/translations-key.md +1 -1
  28. package/assets/rules/angular-patterns.md +5 -0
  29. package/assets/rules/browser-verification.md +17 -12
  30. package/assets/rules/component-structure.md +6 -2
  31. package/assets/rules/doc-style.md +16 -0
  32. package/assets/rules/git-workflow.azure.md +45 -1
  33. package/assets/rules/git-workflow.github.md +52 -1
  34. package/assets/rules/git-workflow.gitlab.md +46 -1
  35. package/assets/rules/lists.md +13 -0
  36. package/assets/rules/observability.md +147 -0
  37. package/assets/rules/ownership-scope.md +5 -2
  38. package/assets/rules/ownership-session.md +124 -0
  39. package/assets/rules/permissions.md +23 -0
  40. package/assets/rules/pricing.md +4 -0
  41. package/assets/rules/reuse-first.md +9 -0
  42. package/assets/rules/seo.md +57 -9
  43. package/assets/rules/shared-code.md +6 -0
  44. package/assets/rules/spec-driven.md +9 -0
  45. package/assets/rules/styling-bem.md +34 -1
  46. package/assets/rules/task-flow.md +5 -0
  47. package/assets/rules/testing.md +46 -8
  48. package/assets/rules/translations.md +11 -5
  49. package/assets/rules/typescript-conventions.md +5 -0
  50. package/package.json +1 -1
  51. package/rt-tools-agent-kit-0.6.0.tgz +0 -0
  52. package/rt-tools-agent-kit-0.5.2.tgz +0 -0
@@ -0,0 +1,46 @@
1
+ # Закон о наблюдаемости
2
+
3
+ Что владелец знает о работе своего приложения. Если о поломке можно узнать только через доступ
4
+ к серверу, владелец узнаёт о ней от гостя и с опозданием.
5
+
6
+ **Ревизия:** 2026-08-13
7
+
8
+ ## Статьи
9
+
10
+ - **Отказ приложения виден владельцу без доступа к серверу.** На сервер владелец не заходит,
11
+ поэтому всё, что видно только оттуда, для него недоступно.
12
+ - **Отказ хранится и после перезапуска приложения, и спустя месяцы.** Поломку разбирают уже
13
+ после того, как она случилась, а вывод приложения к этому времени затирается.
14
+ - **Все отказы одного обращения находятся по одному признаку.** При десятке обращений в секунду
15
+ по времени их не разделить.
16
+ - **Тот, кто наткнулся на отказ, получает номер обращения и может его назвать.** Иначе по жалобе
17
+ нечего искать.
18
+ - **Наружу уходит номер обращения и общий текст ошибки. Подробности остаются внутри.**
19
+ Подробности показывают, как приложение устроено.
20
+ - **Подробности отказа закрыты правом.** В них видно, кто был в системе и чем именно приложение
21
+ сломалось.
22
+ - **Владелец узнаёт о новом отказе сам, без захода на экран.** Иначе поломку заметят только
23
+ тогда, когда до экрана дойдут руки.
24
+ - **У обращения к чужой службе есть предел ожидания.** Молчащая служба отличается от отказавшей
25
+ только временем: без предела обращение висит до умолчания среды, отказ не наступает вовсе, и
26
+ записывать нечего. Всё это время владелец видит исправную работу.
27
+ - **Обращение к чужой службе оставляет запись, и заводится она до обращения.** Запись после
28
+ ответа говорит только о том, что дошло: попытка, оборвавшаяся посередине, не отличается от не
29
+ начатой, а владелец видит лишь удачные исходы.
30
+ - **Отказ не записывается в ущерб работе приложения.** Ответ не ждёт записи, запрос из-за неё не
31
+ падает. Если записать не удалось, это видно.
32
+ - **Приложение не пишет отказы о собственной записи отказов.** Иначе поломка хранилища порождает
33
+ поток, который сам себя разгоняет.
34
+ - **Отказом считается то, что не сработало, а не каждый обслуженный запрос.** Иначе поток чужих
35
+ обращений определяет, сколько приложение о себе хранит.
36
+ - **У хранилища отказов есть предел, и он проверяется до того, как кончится место.** Иначе
37
+ хранилище отказов положит приложение, за которым следит.
38
+ - **Секреты и личные данные вычищаются перед записью.** После записи их убирают правкой данных,
39
+ а это дороже и не всегда возможно.
40
+
41
+ ## Открытые вопросы
42
+
43
+ - **Q-O-1.** Куда девать отказ, случившийся до того, как приложение поднялось. Хранилища в этот
44
+ момент ещё нет. Сейчас такой отказ остаётся только в выводе приложения.
45
+ - **Q-O-2.** Хранить ли отказы обвязки — того, что стоит перед приложением и отдаёт страницы.
46
+ Её вывод к приложению не приходит, и читать его надо отдельно.
@@ -33,6 +33,10 @@
33
33
  — это намерение, а не свойство приложения: сверить его не с чем, и оно проходит любую
34
34
  проверку. Машине это не поручить: открытый вопрос пишется теми же словами, что и обещание,
35
35
  и проверка отбивала бы оба.
36
+ - **Полнота текстов проверяется и со стороны работы, а не только со стороны текста.** Обход
37
+ написанного судит каждое утверждение, но утверждения, которого нет, в этом обходе нет тоже:
38
+ приём, применённый и нигде не описанный, так не находится никогда. Поэтому закрытая работа
39
+ спрашивается отдельно — оставила она след в текстах или явно его не требует.
36
40
  - **Документ утверждает о состоявшемся, а не о том, что должно сработать.** Лечение,
37
41
  записанное готовым до того, как его прогнали, дороже отсутствия записи: следующий читатель
38
42
  берёт его за проверенное — и берёт в тот день, когда лечение понадобилось, а времени на
@@ -8,6 +8,8 @@
8
8
 
9
9
  ## Статьи
10
10
 
11
+ - **У каждого приложения один источник вида, и он у них разный.** Взять контрол из чужого
12
+ источника значит принести на экран форму, которой в этом приложении нет больше нигде.
11
13
  - **Готовое берут, а не пишут заново.** Своя копия расходится с оригиналом с первой же правки,
12
14
  и одинаковые с виду места начинают вести себя по-разному.
13
15
  - **Отойти от общего вида может решить только владелец.** Сделать своё вместо готового
@@ -56,3 +56,8 @@
56
56
  род события и версия одинаковы везде, где стоит слой правил; путь, домен и имя дерева
57
57
  принадлежат одному дереву и в чужом месте не значат ничего, кроме утечки. Держится это
58
58
  проверкой на выносящей стороне, а не памятью того, кто пишет.
59
+ - **Проверка, которая сама сломалась, работу не останавливает.** Отказ инструмента не является
60
+ найденным нарушением, и остановленная им работа стоит до того, как его починят.
61
+ - **Решение, зависящее от текущего момента, получает момент снаружи.** Иначе проверить его можно
62
+ только подкруткой часов, а подкрученные часы действуют и на всё, что оказалось рядом: проверка
63
+ начинает зависеть от того, что к ней отношения не имеет.
@@ -50,12 +50,12 @@ npx nx build admin --base-href=/
50
50
  Angular DevTools, нужна ещё и dev-конфигурация (`--configuration=development`): прод-сборка не
51
51
  публикует `window.ng`. Выводы о размере бандла и минификации на такой сборке делать нельзя.
52
52
 
53
- Сессия кладётся в `localStorage['vm.admin.token']` **строкой JSON** (`JSON.stringify(token)`),
53
+ Сессия кладётся в `localStorage['<ключ сессии админки>']` **строкой JSON** (`JSON.stringify(token)`),
54
54
  иначе приложение её не прочитает. Токен не подписывается руками, а берётся у живого API:
55
55
  `POST /<область>.v1.AuthService/Login`. Команду с паролем классификатор блокирует — обходить не
56
56
  надо, спрашивать разрешение у владельца.
57
57
 
58
- Взять уже открытую сессию нельзя: чтение `localStorage['vm.admin.token']` из браузера
58
+ Взять уже открытую сессию нельзя: чтение того же ключа из браузера
59
59
  блокируется. Оба пути к своему стенду упираются в пароль, поэтому остаётся третий — смотреть
60
60
  на админке владельца, где вход уже сделан. Свой стенд нужен, только когда проверяют
61
61
  прод-сборку, `--base-href` или конфиг nginx; чтобы просто посмотреть экраны, он не нужен.
@@ -131,8 +131,28 @@ pnpm install --frozen-lockfile # из ../<префикс>-base: node_mod
131
131
  - Оба стенда держат поднятыми одновременно: если сравнивать по памяти между двумя запусками,
132
132
  заметишь только то, что успел запомнить.
133
133
 
134
+ ## Состояние портов снимается до первой сборки захода
135
+
136
+ Серверы владельца ложатся и без твоего участия: сайт отдавал 404 на все свои адреса ещё до
137
+ первой сборки захода, а API замолчало посреди него. Без замера «до» падение нельзя ни
138
+ приписать своей сборке, ни снять с неё подозрение.
139
+
140
+ ```bash
141
+ for u in http://localhost:{{apiPort}}/health http://localhost:{{sitePort}}/ http://localhost:{{adminPort}}/; do
142
+ printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$u")" "$u"
143
+ done
144
+ ```
145
+
146
+ Замер — это ответ, а не `lsof`: порт бывает занят собранным артефактом прошлой сессии, и он
147
+ отвечает 200 старым кодом.
148
+
134
149
  ## Частые промахи
135
150
 
151
+ - **Одиночная сборка проекта серверы переживают, и отказываться от неё незачем.** Замерено
152
+ ответом до и после: все порты остались за своими процессами. Осторожность здесь стоит дороже
153
+ проверки — целая команда паттерна `seo-verify` осталась незапущенной ровно потому, что сборку
154
+ сочли опасной, не замерив. Сборка из кэша замером не является: она не собирает вовсе, и видно
155
+ это по её длительности.
136
156
  - Свой дев-сервер не поднимать: сайт на {{sitePort}}, админка на {{adminPort}}, API на {{apiPort}} уже подняты
137
157
  владельцем, и второй экземпляр отбивается гардом.
138
158
  - **Общая сборка глушит все три дев-сервера владельца, а не только API.** После
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: doc-style-trace
3
+ kind: pattern
4
+ rule: doc-style
5
+ description: Паттерн правила doc-style. Брать, когда полнота текстов проверяется со стороны работы, а не со стороны текста — обратный проход по закрытым задачам: признак отбора машиной, чего он не видит, три исхода по каждой задаче, граница «код не правится». Не брать для разбора документа, накопившего список работ, — это паттерн doc-style-sweep.
6
+ ---
7
+
8
+ # Обратный проход: закрытые задачи против текстов
9
+
10
+ Паттерн правила `doc-style`. Что при этом должно быть верно — закон
11
+ `docs/constitution/project-documentation.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Проверяется не то, верен ли текст, а то, есть ли он вообще.
16
+ - Разбирается работа, закрытая до того, как в закрытие задачи вошёл шаг приведения текстов.
17
+ - Волна разбора со стороны текста кончилась, а находок в ней вышло мало.
18
+
19
+ ## Проход со стороны текста не находит того, чего не написали
20
+
21
+ Он читает написанное и судит каждое утверждение; утверждения, которого нет, в обходе нет
22
+ тоже. Со стороны работы находок на порядок больше: в первой части семь находок из восьми
23
+ легли туда, где текста не было вовсе, а не туда, где он устарел.
24
+
25
+ ## Признак отбора — ветка внесла код и не тронула ни одного текста
26
+
27
+ Отбирается машиной и чтения не требует. Что ветка внесла на самом деле, отвечает история, а
28
+ не папка задачи: папка разбирается при закрытии, и таблица следа задачи до главной ветки не
29
+ доезжает вовсе.
30
+
31
+ ```bash
32
+ git log --merges --format='%H %s' <главная ветка> | grep -E 'from [^/]+/<ключ задач>-[0-9]+-'
33
+ git diff --name-only <мерж>^1 <мерж>
34
+ ```
35
+
36
+ Признак считается вычитанием, а не двумя списками путей: код — всё, что не `.md`, текст —
37
+ любой `.md` вне описания прошлого и папок задач. Списки, написанные руками, разошлись по обоим
38
+ краям сразу, и разошлись молча.
39
+
40
+ Список текстов пропустил самые читаемые тексты дерева: тот, что приходит в контекст каждой
41
+ сессии целиком, и тот, что описывает устройство дерева. Три задачи, тронувшие ровно их, попали
42
+ в выборку как бесследные — и во вторую часть прохода попали второй раз, уже как тронувшие
43
+ текст.
44
+
45
+ Список кода пропустил обвязку: гарды, конвейер и файлы выкатки в нём не значились, и две
46
+ задачи, правившие только их, не попали ни в одну выборку вовсе. Нашлись они не признаком, а
47
+ сверкой двух выборок между собой — её и стоит прогнать перед каждой следующей частью:
48
+
49
+ ```bash
50
+ comm -23 <список бесследных> <разобранные первой частью> # кого не прочитал никто
51
+ comm -12 <разобранные первой частью> <список тронувших текст> # кого прочитали дважды
52
+ ```
53
+
54
+ ## Часть назначает самая узкая область задачи, и метки бывает нет вовсе
55
+
56
+ Выборка, которую признак отобрал, одним заходом не читается, и делится она по области —
57
+ метке области у задачи. Меток у задачи бывает несколько; часть назначает самая узкая из них, а
58
+ порядок сужения выбирается один раз на весь проход и записывается в замысел вместе с таблицей
59
+ частей. При обратном порядке самые широкие области забрали бы себе все спорные задачи, и у
60
+ узких не осталось бы почти ничего.
61
+
62
+ Задача без единой метки области не попадает ни в одну часть вовсе — ни по какому порядку
63
+ сужения. Признак её отобрал, читать её некому, и видно это только пересчётом:
64
+
65
+ ```bash
66
+ awk -F'\t' 'NR==FNR{k[$1]=1;next} k[$1] && $2==""{print}' <номера выборки> <задачи с метками>
67
+ ```
68
+
69
+ Такую задачу относят к части по предмету руками, и это решение записывается в разбор: иначе
70
+ следующая часть пересчитает выборку и найдёт её снова непрочитанной.
71
+
72
+ ## Исходов у задачи три, а не два
73
+
74
+ | Исход | Признак |
75
+ | ---------------- | --------------------------------------------------------------------------------------------------------- |
76
+ | следа не требует | приём нигде не повторён: вёрстка своего экрана, разовая правка по просьбе владельца |
77
+ | след есть | утверждение о работе стоит в правиле, паттерне или спеке — в том числе внесённое отдельной задачей следом |
78
+ | следа нет | приём применён и нигде не описан — это и есть находка |
79
+
80
+ Приём, записанный отдельной задачей следом, промахом не является: работа сделана одной
81
+ задачей, запись приёма заведена другой, и в очереди работ видны обе. Отличается это чтением
82
+ соседних по времени задач той же области, а не признаком.
83
+
84
+ Худшего случая признак не видит вовсе: ветка тронула соседний текст и обошла тот, который
85
+ описывает её собственную работу. Такие задачи остаются следующей части прохода.
86
+
87
+ ## Граница «код не правится» и её единственное исключение
88
+
89
+ Нашлось место, где неправ код, — заводится задача, проход идёт дальше. Кодом при этом не
90
+ считается то, что описывает сверяемое правило: комментарий в шапке проверки и заголовок
91
+ теста. Заголовок несёт идентификатор сценария, и без него новый сценарий значится непокрытым
92
+ при живом тесте.
93
+
94
+ ## Находка кладётся в тот слой, которому принадлежит
95
+
96
+ - приём повторён и решается в коде — утверждение правила с привязкой в именах дерева;
97
+ - готовый код и порядок действий — паттерн;
98
+ - обещание, которое видит человек, — сценарий спека домена с прежней нумерацией;
99
+ - расхождение с договорённостью — текст статьи владельцу, файл закона не правится.
100
+
101
+ ## Частые промахи
102
+
103
+ - Состав части взят из прошлого захода, а не пересчитан признаком. Выборки живут в
104
+ скретчпаде сессии и умирают вместе с ней; в дерево они не кладутся — деление на части
105
+ свойство прохода, а не продукта. Пересчёт стоит двух команд выше и даёт тот же список, а
106
+ состав, принятый на слово, нечем сверить с соседними частями — именно сверкой находятся те,
107
+ кого не прочитал никто.
108
+ - Задача судится по своей папке: её разобрали при закрытии, и следа там не осталось.
109
+ - «След есть» по одному упоминанию: упоминание в соседнем правиле приёма не описывает.
110
+ - Находка записана только в паттерн: правило читают перед каждой правкой, паттерн — по имени.
111
+ - Признак пересчитан на новом списке путей без сверки на выборке руками.
@@ -168,7 +168,7 @@ PR [<КЛЮЧ>-86] Письмо владельцу с незаполнен
168
168
  задача [<КЛЮЧ>-101] Вернуть оверлей загрузки таблицы и включить stylelint гейтом
169
169
  PR [<КЛЮЧ>-101] Stylelint включён гейтом
170
170
 
171
- задача [<КЛЮЧ>-212] Сайт не собирается: компонентам кита проставлен префикс vm- вместо rt-
171
+ задача [<КЛЮЧ>-212] Сайт не собирается: компонентам кита проставлен префикс приложения вместо своего
172
172
  PR [<КЛЮЧ>-212] Виджет переписки зовёт кит его собственными именами
173
173
  ```
174
174
 
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: git-workflow-docker
3
+ kind: pattern
4
+ rule: git-workflow
5
+ description: Паттерн правила git-workflow. Брать при работе с образами на своей машине — подъём и перезапуск демона, диагностика «висящей» команды, сборка под платформу прод-сервера, одноразовый контейнер рядом с чужими, вход в реестр из службы. Не брать для команд прод-сервера и выбора образа — это паттерн git-workflow-restart, и не для наката миграций — это git-workflow-migration.
6
+ ---
7
+
8
+ # Образы на своей машине
9
+
10
+ Паттерн правила `git-workflow`. Что при этом должно быть верно — закон
11
+ `docs/constitution/delivery.md`.
12
+
13
+ Демон здесь чужой: на машине владельца в нём живут его хранилище разработки, его стенд и
14
+ контейнеры других его работ. Любая команда пишется так, чтобы её отменяли, не спрашивая
15
+ владельца, и чтобы она не задела ничего, кроме заведённого ею самой.
16
+
17
+ ## Когда брать
18
+
19
+ - Команда демона не отвечает, и надо понять почему.
20
+ - Нужен одноразовый контейнер рядом с уже работающими.
21
+ - Собирается образ, который поедет на прод-сервер.
22
+ - Раннер конвейера на этой машине не может войти в реестр.
23
+
24
+ ## Чужое не трогается
25
+
26
+ Перед любым действием, которое задевает демон целиком, читается, что в нём живёт и переживёт
27
+ ли оно перезапуск:
28
+
29
+ ```bash
30
+ docker ps --format '{{.Names}} | {{.Image}} | {{.Status}}'
31
+ docker inspect <контейнер> --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}'
32
+ ```
33
+
34
+ `unless-stopped` поднимется сам, `no` — нет, и его возвращают руками сразу после подъёма
35
+ демона. Стенд владельца обычно заведён с `no`: после перезапуска он остаётся лежать, а по его
36
+ порту отвечает пустота — это выглядит поломкой стенда, а не следом перезапуска.
37
+
38
+ Свои контейнеры именуются приставкой и снимаются по имени. `docker system prune`, `docker rm`
39
+ по маске и `docker volume prune` не пишутся никогда: они уносят чужое молча.
40
+
41
+ ```bash
42
+ docker rm -f <приставка>-ci-db >/dev/null 2>&1 || true # снять прошлый прогон
43
+ ```
44
+
45
+ Порт своего контейнера выбирается свободным: порт хранилища разработки занят, и попасть в него
46
+ чужой миграцией нельзя.
47
+
48
+ ```bash
49
+ lsof -nP -iTCP:<свободный порт> -sTCP:LISTEN # пусто — порт свободен
50
+ ```
51
+
52
+ ## Место спрашивается до прогона
53
+
54
+ Диск виртуальной машины отдельный от диска хоста: на хосте свободно, внутри пусто, и видно это
55
+ только изнутри.
56
+
57
+ ```bash
58
+ docker system df # образы, тома и кэш сборки с долей многоразового
59
+ docker run --rm alpine:3 df -h / # сколько осталось у самой виртуальной машины
60
+ ```
61
+
62
+ Наружу нехватка выходит чужим лицом: контейнер поднимается и сразу гаснет, сверка схемы
63
+ отвечает про ненакатываемую цепочку, гейт пуша краснеет целиком. По этим признакам чинят
64
+ репозиторий, а причина в машине, — поэтому первое при любом из них `docker system df`.
65
+
66
+ Освобождают отбором, а не общей чисткой: у сценария чистки образов сперва спрашивают, что он
67
+ снял бы, и только потом дают снимать.
68
+
69
+ ## Демон поднимается своим CLI
70
+
71
+ Открытие приложения виртуальную машину не поднимает: приложение считается запущенным, а демон
72
+ не отвечает часами. Поднимает только собственная команда клиента, а готовность проверяется
73
+ самим демоном:
74
+
75
+ ```bash
76
+ docker desktop restart
77
+ until docker info >/dev/null 2>&1; do sleep 5; done
78
+ docker version --format 'демон: {{.Server.Version}}'
79
+ ```
80
+
81
+ Состояние, которое печатает приложение, говорит про приложение: оно отвечает «работает» и
82
+ тогда, когда демон не принимает ни одной команды. Единственный признак живого демона — ответ
83
+ `docker info`. Первые полминуты после подъёма он отвечает ошибкой и пишет в лог, что маршрута
84
+ до виртуальной машины нет: это нормальный старт, а не поломка.
85
+
86
+ ## «Команда висит» — сначала проверяется, вправду ли висит
87
+
88
+ Вывод не заворачивается в `tail`, `head` и не глушится тихим режимом: они держат его в буфере
89
+ до конца команды, и идущая работа выглядит зависшей. Читается прямой вывод:
90
+
91
+ ```bash
92
+ docker pull alpine:3 # прогресс виден построчно
93
+ ```
94
+
95
+ Скачивания не запускаются параллельно. Несколько одновременных забивают канал друг другу:
96
+ образ, который тянется за три секунды, шёл полчаса — и это выглядело сломанным демоном, а было
97
+ очередью, устроенной проверяющим.
98
+
99
+ ## Где рвётся: три яруса
100
+
101
+ Ярусы проверяются по отдельности, иначе чинится не то. Каждый отвечает секундами:
102
+
103
+ ```bash
104
+ # 1. Сеть машины: реестр отдаёт манифест
105
+ curl -s -m 30 -w '%{http_code} за %{time_total}s\n' -o /dev/null '<адрес токена реестра>'
106
+
107
+ # 2. Сеть контейнеров: объём проходит внутрь
108
+ docker run --rm alpine:3 sh -c 'time wget -q -O /dev/null <адрес пробы канала>'
109
+
110
+ # 3. Демон: тянет ли он сам
111
+ docker pull busybox:latest
112
+ ```
113
+
114
+ Хост тянет быстро, контейнер тянет быстро, а скачивание стоит — дело в демоне, и его
115
+ перезапускают. Стоят все три — дело в сети машины, и демон ни при чём. Что делал сам демон,
116
+ отвечают его логи: ходы к реестру, подъём и состояние, консоль виртуальной машины — три разных
117
+ файла в каталоге данных клиента.
118
+
119
+ ## Команда из службы: свой каталог настроек и явный адрес демона
120
+
121
+ Раннер конвейера запущен службой, и вход в реестр из неё отказывает: пароль сохраняет
122
+ системный помощник хранения ключей, а сеанса пользователя у службы нет. Свой каталог настроек
123
+ с пустым помощником от этого не спасает — клиент подставляет помощника сам и переписывает
124
+ пустое значение молча. Поэтому вход не зовётся вовсе, а пароль пишется в файл настроек прямо:
125
+
126
+ ```bash
127
+ export DOCKER_CONFIG="$(mktemp -d)"
128
+ export DOCKER_HOST="unix://${HOME}/.docker/run/docker.sock"
129
+ auth=$(printf '%s:%s' "${REGISTRY_USER}" "${REGISTRY_TOKEN}" | base64)
130
+ printf '{"auths":{"<реестр>":{"auth":"%s"}}}' "${auth}" > "${DOCKER_CONFIG}/config.json"
131
+ chmod 600 "${DOCKER_CONFIG}/config.json"
132
+ ```
133
+
134
+ `DOCKER_HOST` здесь не для красоты: свой каталог уносит с собой и текущий контекст, а без него
135
+ клиент идёт в общесистемный сокет, которого на машине с настольным клиентом нет вовсе, и
136
+ отвечает «нет такого файла» на что угодно. Связь проверяется до сборки:
137
+
138
+ ```bash
139
+ docker info --format 'демон: {{.ServerVersion}}'
140
+ ```
141
+
142
+ Поле у `docker info` называется иначе, чем у `docker version`: имя из второй команды первая не
143
+ понимает, и связь выглядит непроверенной, хотя демон отвечает.
144
+
145
+ Что пароль принят, видно по ответу реестра: анонимному он отвечает `401`, авторизованному —
146
+ содержимым или `403`, но не `401`. Каталог снимается в конце — раннер живёт между прогонами, и
147
+ пароль реестра остался бы лежать на диске владельца.
148
+
149
+ ## Образ собирается под платформу прод-сервера
150
+
151
+ Машина владельца и прод-сервер бывают разной архитектуры. Без явной платформы собирается образ
152
+ под сборщика: он уходит в реестр, оттуда на сервер и не стартует там вовсе.
153
+
154
+ ```bash
155
+ docker buildx build --platform <платформа сервера> -f <файл сборки> --output type=cacheonly .
156
+ ```
157
+
158
+ `--output type=cacheonly` считает сборку, ничего не сохраняя, — этим замеряют время, не
159
+ засоряя машину образом. Чужая платформа идёт эмуляцией, поэтому время сборки на машине и в
160
+ облаке сравнивают числом, а не ожиданием.
161
+
162
+ Сборщик с драйвером `docker-container` нужен для чужой платформы и заводится отдельно; его
163
+ первый подъём тянет свой образ из реестра:
164
+
165
+ ```bash
166
+ docker buildx create --name <приставка>-ci-builder --driver docker-container
167
+ docker buildx inspect <приставка>-ci-builder --bootstrap # покажет платформы и состояние
168
+ docker buildx build --builder <приставка>-ci-builder … # сборщик владельца не переключается
169
+ ```
170
+
171
+ Сборщик не сносится после прогона: в его кэше живут слои и хранилище пакетов, а без них
172
+ следующая сборка тянет все зависимости заново. Флаг `--use` тоже не пишется — он переключает
173
+ сборщик владельца; вместо него `--builder` у самой сборки. Объявления сборщиков лежат в
174
+ каталоге настроек, поэтому под своим каталогом их не видно вовсе — оставить их на месте
175
+ помогает `BUILDX_CONFIG` с адресом каталога владельца.
176
+
177
+ Установка зависимостей внутри сборки ограничивается по числу запросов, иначе она роняет сборку
178
+ целиком — и не потому, что канал медленный, а потому, что сотни запросов забивают его сами
179
+ себе.
180
+
181
+ ## Частые промахи
182
+
183
+ - **Перезапуск демона объявлен сделанным по состоянию приложения.** Приложение говорит
184
+ «работает», а `docker info` в это же время отвечает ошибкой.
185
+ - **Стенд не возвращён после перезапуска.** У него политика `no`, и владелец находит мёртвый
186
+ порт вместо стенда.
187
+ - **Вывод команды заведён в `tail`** — и работающая команда объявлена зависшей.
188
+ - **Несколько скачиваний разом** — и медленной объявлена машина, а не собственная очередь.
189
+ - **Образ собран без указания платформы** — прод получает образ чужой архитектуры, и видно это
190
+ только на перезапуске контейнеров.
191
+ - **Свой контейнер занял порт хранилища разработки** — конвейер пишет в данные владельца.
192
+ - **Вход в реестр из службы сделан командой входа** — пароль уходит в помощник хранения ключей
193
+ даже из своего каталога настроек, и задание падает до сборки.
194
+ - **Каталог настроек подменён, а адрес демона не задан** — и починка входа в реестр выглядит
195
+ как упавший демон.
196
+ - **Сборщик снесён после прогона** — вместе с кэшем, и следующая сборка идёт как первая.
197
+ - **Общая чистка ради места** — уносит чужие образы и тома, и восстановить их нечем.
198
+ - **Место освобождено сценарием выкатки целиком.** Сценарий писан для прод-сервера: там
199
+ собирает конвейер, а сервер только тянет готовое, поэтому последним шагом сценарий сносит
200
+ кэш сборщика. На машине владельца собирает раннер, и тот же шаг оставляет сборщика без кэша.
201
+ Отбор самих образов у сценария годится и здесь — он идёт по имени своего реестра, оставляет
202
+ три последних sha и обходит поднятые контейнеры, — а вот его хвост на этой машине запускают,
203
+ только когда согласились ждать полную сборку.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: git-workflow-secrets
3
+ kind: pattern
4
+ rule: git-workflow
5
+ description: Паттерн правила git-workflow. Брать при работе с ключами внешних служб — где они лежат, чем ключ, заводимый владельцем, отличается от ключа окружения, что означает каждое состояние строки интеграции и почему зелёная проба не обещает работающей возможности. Не брать для перезапуска прода и выбора образа — это паттерн git-workflow-restart.
6
+ ---
7
+
8
+ # Ключи внешних служб
9
+
10
+ Паттерн правила `git-workflow`. Что при этом должно быть верно — закон
11
+ `docs/constitution/delivery.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Возможность, которая ходит наружу, молчит: не уходят письма, не обновляются переводы.
16
+ - Заводится или меняется ключ внешней службы.
17
+ - Разбирается, что именно выкачено и чего приложению не хватает для работы, — включая ключ,
18
+ который живёт в окружении и экрана не имеет.
19
+
20
+ ## Ключ, заводимый владельцем, живёт в хранилище, а не в окружении
21
+
22
+ Такой ключ лежит строкой в хранилище, зашифрованной ключом шифрования секретов; рядом открытая
23
+ подсказка из последних знаков и исход последней пробы. Читает его одна служба и отдаёт тем, кто
24
+ ходит наружу.
25
+
26
+ **Запасного пути на окружение нет.** Одноимённая переменная в составе прода ничего не значит:
27
+ приложение её не читает. Из окружения берётся один ключ — тот, которым шифруются все
28
+ остальные. Его потеря делает записанные ключи нечитаемыми, и заводить их заново бесполезно,
29
+ пока он не вернётся.
30
+
31
+ Заводит такие ключи владелец сам, экраном интеграций. **Агент ключи не вводит:** ввод ключа
32
+ доступа в поле ему запрещён независимо от того, кто просит.
33
+
34
+ ## Не всякий ключ внешней службы заводится владельцем
35
+
36
+ В хранилище живут ключи, которые владелец заводит сам и по-разному у каждого владения. Ключ,
37
+ одинаковый для всего приложения, остаётся в окружении, и экрана интеграций у него нет.
38
+
39
+ Половины такой пары — ключ бэкенда и ключ, вшитый в сборку, — лежат по разные стороны
40
+ поставки, и заполнить можно ровно одну. Тогда возможность не выключена и не включена: виджет
41
+ не рисуется, сервер ждёт токен. Приложение называет такое состояние сломанным и говорит о нём
42
+ строкой лога и сводкой старта — что в ней стоит, описывает правило `observability`.
43
+
44
+ Отсюда порядок разбора для ключа из окружения: он читается не экраном интеграций, а сводкой
45
+ старта в логах контейнера — в каком из её списков стоит имя возможности.
46
+
47
+ ## Строка интеграции говорит пятью состояниями
48
+
49
+ Порядок разбора важен — состояние хранилища перекрывает всё остальное:
50
+
51
+ | Что показано | Что это значит |
52
+ | ------------------------------------ | ---------------------------------------------------------------------- |
53
+ | состояние неизвестно | ответ ещё не пришёл; утверждать нечего |
54
+ | хранилище недоступно | ключа шифрования нет; заводить ключи нельзя, и владелец тут ни при чём |
55
+ | не задан | строки секрета нет вовсе — ключ никогда не заводили |
56
+ | расшифровать нечем | строка есть, а ключ шифрования сменился; заводить заново бесполезно |
57
+ | не проверен · работает · не работает | ключ записан; дальше судит проба |
58
+
59
+ Отсюда короткий путь разбора: молчит письмо или перевод — сначала открыть этот экран, а не
60
+ логи. «Не задан» отвечает на вопрос целиком, и десять дней молчащей почты выяснились именно
61
+ им, а не сервером.
62
+
63
+ ## Зелёная проба обещает меньше, чем кажется
64
+
65
+ Проба спрашивает у службы то, что та отдаёт по ключу: список доменов, список моделей. Ни
66
+ письма, ни запроса за деньги она не делает — но и работоспособности возможности не доказывает:
67
+
68
+ - список моделей отдаётся и при пустом балансе, а сам запрос отвечает отказом по деньгам.
69
+ Строка при этом зелёная;
70
+ - проба почты сверяет адрес отправителя со списком подтверждённых — но только если адрес уже
71
+ заполнен. При пустом адресе она отвечает «работает», а письма не уйдут.
72
+
73
+ Поэтому после ввода ключа проверяется сама возможность, а не строка: сохранить запись и
74
+ убедиться, что предупреждение не пришло; дождаться первого обращения и увидеть письмо.
75
+
76
+ ## Переезд ключа из окружения в хранилище не делается миграцией
77
+
78
+ Секрет шифруется приложением, поэтому запросом к хранилищу его не перенести: миграция заводит
79
+ таблицу и не трогает значения. Ключ, переведённый из окружения в настройки, обязан быть
80
+ заведён владельцем в тот же день, что выкачена правка, — иначе возможность замолкает молча.
81
+ Сверяется тем же экраном интеграций сразу после выкатки.
82
+
83
+ ## Частые промахи
84
+
85
+ - **Искать ключ поиском по составу прода и делать вывод.** Значение там лежит, приложение его
86
+ не читает, а хвост из последних знаков совпадает с записанным в хранилище — совпадение
87
+ подсказки и переменной ничего не доказывает, кроме того, что владелец завёл тот же ключ.
88
+ - **Идти в логи прежде экрана.** Логи скажут про отказ внешней службы, а экран — «не задан»;
89
+ второе точнее и стоит одного нажатия.
90
+ - **Считать отсутствие ошибок признаком работы.** Ни почта, ни перевод не роняют запрос:
91
+ обращение сохранится, запись сохранится, а наружу ничего не уйдёт.
92
+ - **Заводить ключ заново при «расшифровать нечем».** Это не про ключ, а про ключ шифрования;
93
+ новый ляжет рядом и тоже не прочитается.