[{"data":1,"prerenderedAt":1730},["ShallowReactive",2],{"navigation_docs":3,"-modules-options-build-and-install":238,"-modules-options-build-and-install-surround":1727},[4,45,60],{"title":5,"description":6,"icon":7,"path":8,"stem":9,"children":10},"Методология",null,"ruler","\u002Fmethodology","1.methodology",[11,15,20,25,30,35,40],{"title":12,"path":8,"stem":13,"description":14},"bxshef: методология и проверка навыков","1.methodology\u002Findex","Как писать навыки ИИ-агентов для Битрикса и как проверять, что им можно верить",{"title":16,"path":17,"stem":18,"description":19},"Стандарт навыка","\u002Fmethodology\u002Fstandard","1.methodology\u002F1.standard","11 правил, каждое из провала на стенде",{"title":21,"path":22,"stem":23,"description":24},"Методология проверки","\u002Fmethodology\u002Fmethod","1.methodology\u002F2.method","lint → eval → стенд; что измерено",{"title":26,"path":27,"stem":28,"description":29},"bxshef — CLI","\u002Fmethodology\u002Fbxshef","1.methodology\u002F3.bxshef","lint, eval, feedback",{"title":31,"path":32,"stem":33,"description":34},"GitHub Action","\u002Fmethodology\u002Faction","1.methodology\u002F4.action","тот же lint + eval в любом репозитории навыков",{"title":36,"path":37,"stem":38,"description":39},"Заготовка репозитория","\u002Fmethodology\u002Ftemplate","1.methodology\u002F5.template","с чего начать автору модуля",{"title":41,"path":42,"stem":43,"description":44},"Приёмник отзывов","\u002Fmethodology\u002Ffeedback","1.methodology\u002F6.feedback","куда уходят отзывы ИИ-агентов",{"title":46,"description":6,"icon":47,"path":48,"stem":49,"children":50},"Навыки","sparkles","\u002Fskills","2.skills",[51,55],{"title":52,"path":48,"stem":53,"description":54},"Навыки shef.*","2.skills\u002Findex","npx skills add bx-shef\u002Fskills",{"title":56,"path":57,"stem":58,"description":59},"shef-feedback","\u002Fskills\u002Fshef-feedback","2.skills\u002F1.shef-feedback","Отправить отзыв о навыке shef-* или навыке команды. Брать ВСЕГДА, когда просят оставить или отправить отзыв о навыке — даже если сам навык в диалоге не виден, —",{"title":61,"description":6,"icon":62,"path":63,"stem":64,"children":65},"Модули shef.*","package","\u002Fmodules","3.modules",[66,69,131,179],{"title":61,"path":63,"stem":67,"description":68},"3.modules\u002Findex","Что за чем ставить",{"title":70,"description":6,"path":71,"stem":72,"children":73},"shef.options","\u002Fmodules\u002Foptions","3.modules\u002F1.options\u002Findex",[74,76,81,86,91,96,101,106,110,114,119,123,127],{"title":70,"path":71,"stem":72,"description":75},"фундамент: настройки, трейты, компоненты",{"title":77,"path":78,"stem":79,"description":80},"[`\\Shef\\Options\\Installator`] Installer","\u002Fmodules\u002Foptions\u002Finstaller","3.modules\u002F1.options\u002F1.installer","Для облегчения установок.",{"title":82,"path":83,"stem":84,"description":85},"Сборка, CI и релиз","\u002Fmodules\u002Foptions\u002Fbuild-and-install","3.modules\u002F1.options\u002F10.build-and-install","Раскладка репозитория — в module-structure.md, процесс —\nв CONTRIBUTING.md, установка глазами пользователя —\nв README.md.",{"title":87,"path":88,"stem":89,"description":90},"Раскладка репозитория","\u002Fmodules\u002Foptions\u002Fmodule-structure","3.modules\u002F1.options\u002F11.module-structure","Файл про устройство репозитория. Опорные точки модуля — в CLAUDE.md,\nпроцесс — в CONTRIBUTING.md, сборка — в\nbuild-and-install.md.",{"title":92,"path":93,"stem":94,"description":95},"Проверка на портале","\u002Fmodules\u002Foptions\u002Fportal-check","3.modules\u002F1.options\u002F12.portal-check","Всё, что ниже рантайма Битрикса, тестами не закрыть: установка, права, кеш,\nраскладка файлов, поведение при обновлении. Проверять это приходится руками —\nи лучше по списку, потому что забытый шаг находит не разработчик, а клиент.",{"title":97,"path":98,"stem":99,"description":100},"[`\\Shef\\Options\\Main\\Options`] Опции настроек модуля","\u002Fmodules\u002Foptions\u002Foptions","3.modules\u002F1.options\u002F2.options","Поддерживает следующие типы:",{"title":102,"path":103,"stem":104,"description":105},"Работа с пользователями","\u002Fmodules\u002Foptions\u002Fsecurity","3.modules\u002F1.options\u002F3.security","",{"title":107,"path":108,"stem":109,"description":105},"[`\\Shef\\Options\\Main\\Utils`] Утилиты","\u002Fmodules\u002Foptions\u002Futils","3.modules\u002F1.options\u002F4.utils",{"title":111,"path":112,"stem":113,"description":105},"[`\\Shef\\Options\\Components`] Работа с компонентами","\u002Fmodules\u002Foptions\u002Fcomponents","3.modules\u002F1.options\u002F5.components",{"title":115,"path":116,"stem":117,"description":118},"[`\\Shef\\Options\\Options`] Паттерны","\u002Fmodules\u002Foptions\u002Fpattern","3.modules\u002F1.options\u002F6.pattern","Запускаемые примеры: singleton.php,\nconfig.php, smartstd.php —\nкак их гонять, написано в examples\u002FREADME.md.",{"title":120,"path":121,"stem":122,"description":105},"[`\\Shef\\Options\\Tests`] Тестирование","\u002Fmodules\u002Foptions\u002Ftests","3.modules\u002F1.options\u002F7.tests",{"title":124,"path":125,"stem":126,"description":105},"[`\\Shef\\Options\\TraitList`] Набор трейтов","\u002Fmodules\u002Foptions\u002Ftraitlist","3.modules\u002F1.options\u002F8.traitlist",{"title":128,"path":129,"stem":130,"description":105},"Правила для ИИ-агентов в этом репозитории","\u002Fmodules\u002Foptions\u002Fagent-rules","3.modules\u002F1.options\u002F9.agent-rules",{"title":132,"description":6,"path":133,"stem":134,"children":135},"shef.problems","\u002Fmodules\u002Fproblems","3.modules\u002F2.problems\u002Findex",[136,138,143,147,152,157,161,166,169,172,175],{"title":132,"path":133,"stem":134,"description":137},"логи, журнал событий, учёт проблем",{"title":139,"path":140,"stem":141,"description":142},"[`\\Shef\\Problems\\Integration`] События и меню","\u002Fmodules\u002Fproblems\u002Fevents","3.modules\u002F2.problems\u002F1.events","Что модуль регистрирует в ядре при установке. Список — в .settings.php, ключ\ninstallEvents; что каждый обработчик существует, сверяет\ntests\u002Fsettings_test.php.",{"title":144,"path":145,"stem":146,"description":105},"Безопасность логов","\u002Fmodules\u002Fproblems\u002Fsecurity","3.modules\u002F2.problems\u002F10.security",{"title":148,"path":149,"stem":150,"description":151},"Быстрая отладка: `_pr`, `_log`, `_log1`","\u002Fmodules\u002Fproblems\u002Fdeffunctions","3.modules\u002F2.problems\u002F2.deffunctions","Функции объявляет def-functions.php, подключается он из include.php.",{"title":153,"path":154,"stem":155,"description":156},"Уровни логирования","\u002Fmodules\u002Fproblems\u002Floglevel","3.modules\u002F2.problems\u002F3.loglevel","Уровни — стандарт PSR-3, в Monolog это \\Monolog\\Level. Ниже — когда какой брать.",{"title":158,"path":159,"stem":160,"description":105},"[`\\Shef\\Problems\\Integration\\Monolog`] Monolog","\u002Fmodules\u002Fproblems\u002Fmonolog","3.modules\u002F2.problems\u002F4.monolog",{"title":162,"path":163,"stem":164,"description":165},"Ротация логов","\u002Fmodules\u002Fproblems\u002Flogrotate","3.modules\u002F2.problems\u002F5.logrotate","Логи модуля лежат в каталоге логов — по умолчанию на уровень выше корня сайта,\nдля BitrixVM \u002Fhome\u002Fbitrix\u002Fsh_log\u002F*.log, — и сами не чистятся. Ротацию делает\nlogrotate — системная утилита, а не Monolog: Monolog тоже умеет\n(RotatingFileHandler), но тогда за файлами следит каждый PHP-процесс, а не\nодна служба.",{"title":128,"path":167,"stem":168,"description":105},"\u002Fmodules\u002Fproblems\u002Fagent-rules","3.modules\u002F2.problems\u002F6.agent-rules",{"title":82,"path":170,"stem":171,"description":85},"\u002Fmodules\u002Fproblems\u002Fbuild-and-install","3.modules\u002F2.problems\u002F7.build-and-install",{"title":87,"path":173,"stem":174,"description":90},"\u002Fmodules\u002Fproblems\u002Fmodule-structure","3.modules\u002F2.problems\u002F8.module-structure",{"title":92,"path":176,"stem":177,"description":178},"\u002Fmodules\u002Fproblems\u002Fportal-check","3.modules\u002F2.problems\u002F9.portal-check","Всё, что ниже рантайма Битрикса, тестами не закрыть: установка, права, меню,\nраскладка файлов, журнал событий, поведение при обновлении. Проверять это\nприходится руками — и лучше по списку, потому что забытый шаг находит не\nразработчик, а клиент.",{"title":180,"description":6,"path":181,"stem":182,"children":183},"shef.insync","\u002Fmodules\u002Finsync","3.modules\u002F3.insync\u002Findex",[184,186,191,194,198,203,208,213,218,222,227,232,235],{"title":180,"path":181,"stem":182,"description":185},"агенты, импорт, API-клиенты, модели",{"title":187,"path":188,"stem":189,"description":190},"[`\\Shef\\InSync\\Agents`] Агенты","\u002Fmodules\u002Finsync\u002Fagents","3.modules\u002F3.insync\u002F1.agents","Агент импорта — наследник \\Shef\\InSync\\Agents\\AAgent. Описание агента для\nb_agent — \\Shef\\InSync\\Agents\\Entity, установку и управление берёт на себя\n\\Shef\\InSync\\Agents\\Manager.",{"title":87,"path":192,"stem":193,"description":90},"\u002Fmodules\u002Finsync\u002Fmodule-structure","3.modules\u002F3.insync\u002F10.module-structure",{"title":92,"path":195,"stem":196,"description":197},"\u002Fmodules\u002Finsync\u002Fportal-check","3.modules\u002F3.insync\u002F11.portal-check","Всё, что ниже рантайма Битрикса, тестами не закрыть: установка, права, левое\nменю, раскладка компонентов, агенты, таблица импорта, поведение при\nобновлении. Проверять это приходится руками — и лучше по списку, потому что\nзабытый шаг находит не разработчик, а клиент.",{"title":199,"path":200,"stem":201,"description":202},"Безопасность","\u002Fmodules\u002Finsync\u002Fsecurity","3.modules\u002F3.insync\u002F12.security","Модуль даёт другим модулям заготовки для синхронизаций, и самые опасные места\nу него общие с ними: кто может запускать агенты и импорт, что ложится в каталоги\nимпорта, что уходит в SQL и в лог. Ниже — что держит модуль и что остаётся\nпроекту.",{"title":204,"path":205,"stem":206,"description":207},"[`\\Shef\\InSync\\Sync`] Импорт","\u002Fmodules\u002Finsync\u002Fimport","3.modules\u002F3.insync\u002F2.import","Импорт идёт в два шага через таблицу импорта \\Shef\\InSync\\Sync\\Model\\SyncTable\n(shef_insync_model):",{"title":209,"path":210,"stem":211,"description":212},"[`\\Shef\\InSync\\Api`] API","\u002Fmodules\u002Finsync\u002Fapi","3.modules\u002F3.insync\u002F3.api","Обращение к внешнему API по HTTP — наследник \\Shef\\InSync\\Api\\AConnector.",{"title":214,"path":215,"stem":216,"description":217},"[`\\Shef\\InSync\\TraitList\\Xml`] Парсинг XML","\u002Fmodules\u002Finsync\u002Fxml","3.modules\u002F3.insync\u002F4.xml","Разбор XML сделан через SbWereWolf\u002Fxml-navigator\n(статья на Хабре): XMLReader идёт по\nдокументу потоком, а каждый элемент с нужным тегом HierarchyComposer\nпревращает в массив — n имя, v значение, a атрибуты, s вложенные\nэлементы.",{"title":219,"path":220,"stem":221,"description":105},"Компоненты","\u002Fmodules\u002Finsync\u002Fcomponents","3.modules\u002F3.insync\u002F5.components",{"title":223,"path":224,"stem":225,"description":226},"Страницы","\u002Fmodules\u002Finsync\u002Fpage","3.modules\u002F3.insync\u002F6.page","Страницы модуля — в левом меню, штатным разделом intranet\n(Bitrix\\Intranet\\CustomSection): раздел «SH Импорт», код shinsync.\nВерхней панели и shef.uiclear больше нет.",{"title":228,"path":229,"stem":230,"description":231},"[`\\Shef\\InSync\\Main\\Options`] Опции настроек модуля","\u002Fmodules\u002Finsync\u002Foptions","3.modules\u002F3.insync\u002F7.options","Опции для страницы настроек модуля импорта (на ShOptionsConfig из\nshef.options): добавьте их во вкладку своего options_conf.php.",{"title":128,"path":233,"stem":234,"description":105},"\u002Fmodules\u002Finsync\u002Fagent-rules","3.modules\u002F3.insync\u002F8.agent-rules",{"title":82,"path":236,"stem":237,"description":85},"\u002Fmodules\u002Finsync\u002Fbuild-and-install","3.modules\u002F3.insync\u002F9.build-and-install",{"id":239,"title":82,"body":240,"description":85,"extension":1721,"meta":1722,"navigation":1723,"path":83,"rawbody":1724,"seo":1725,"stem":84,"__hash__":1726},"docs\u002F3.modules\u002F1.options\u002F10.build-and-install.md",{"type":241,"value":242,"toc":1701},"minimark",[243,263,272,341,349,353,395,406,432,461,470,491,508,518,522,699,705,710,716,756,772,776,792,812,816,830,833,965,969,975,1045,1050,1056,1064,1087,1094,1104,1108,1114,1134,1150,1153,1159,1176,1199,1202,1234,1238,1249,1293,1300,1312,1322,1328,1354,1360,1427,1432,1436,1454,1464,1468,1499,1511,1528,1546,1552,1627,1647,1650,1667,1670,1678,1685,1697],[244,245,246,247,251,252,258,259,262],"p",{},"Раскладка репозитория — в ",[248,249,250],"a",{"href":88},"module-structure.md",", процесс —\nв ",[248,253,257],{"href":254,"rel":255},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions\u002Fblob\u002Fmain\u002FCONTRIBUTING.md",[256],"nofollow","CONTRIBUTING.md",", установка глазами пользователя —\nв ",[248,260,261],{"href":71},"README.md",".",[264,265,267,271],"h2",{"id":266},"buildsh-единственная-точка-входа-сборки",[268,269,270],"code",{},"build.sh"," — единственная точка входа сборки",[273,274,278],"pre",{"className":275,"code":276,"language":277,"meta":105,"style":105},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark",".\u002Fbuild.sh            # проверки + архив shef.options.zip\n.\u002Fbuild.sh --check    # только проверки\n.\u002Fbuild.sh --version  # напечатать версию модуля\n.\u002Fbuild.sh --notes    # примечания к релизу из CHANGELOG\n.\u002Fbuild.sh --notes 3.0.6   # то же, но отсчёт от указанного выпуска\n","bash",[268,279,280,293,305,316,327],{"__ignoreMap":105},[281,282,285,289],"span",{"class":283,"line":284},"line",1,[281,286,288],{"class":287},"sbgvK",".\u002Fbuild.sh",[281,290,292],{"class":291},"sutJx","            # проверки + архив shef.options.zip\n",[281,294,296,298,302],{"class":283,"line":295},2,[281,297,288],{"class":287},[281,299,301],{"class":300},"stzsN"," --check",[281,303,304],{"class":291},"    # только проверки\n",[281,306,308,310,313],{"class":283,"line":307},3,[281,309,288],{"class":287},[281,311,312],{"class":300}," --version",[281,314,315],{"class":291},"  # напечатать версию модуля\n",[281,317,319,321,324],{"class":283,"line":318},4,[281,320,288],{"class":287},[281,322,323],{"class":300}," --notes",[281,325,326],{"class":291},"    # примечания к релизу из CHANGELOG\n",[281,328,330,332,334,338],{"class":283,"line":329},5,[281,331,288],{"class":287},[281,333,323],{"class":300},[281,335,337],{"class":336},"srdBf"," 3.0.6",[281,339,340],{"class":291},"   # то же, но отсчёт от указанного выпуска\n",[244,342,343,344,348],{},"CI зовёт ",[345,346,347],"strong",{},"её же",". Это не украшение: если бы сервер гонял свой набор команд,\nлокальный зелёный прогон и серверный красный означали бы разные вещи, и\nразбираться пришлось бы в двух местах сразу.",[264,350,352],{"id":351},"линтер-рядом-а-не-внутри","Линтер — рядом, а не внутри",[273,354,356],{"className":275,"code":355,"language":277,"meta":105,"style":105},"composer install           # поднять инструменты разработчика\ncomposer run lint          # сухой прогон: покажет диф и упадёт\ncomposer run lint:fix      # привести файлы\n",[268,357,358,370,383],{"__ignoreMap":105},[281,359,360,363,367],{"class":283,"line":284},[281,361,362],{"class":287},"composer",[281,364,366],{"class":365},"s_sjI"," install",[281,368,369],{"class":291},"           # поднять инструменты разработчика\n",[281,371,372,374,377,380],{"class":283,"line":295},[281,373,362],{"class":287},[281,375,376],{"class":365}," run",[281,378,379],{"class":365}," lint",[281,381,382],{"class":291},"          # сухой прогон: покажет диф и упадёт\n",[281,384,385,387,389,392],{"class":283,"line":307},[281,386,362],{"class":287},[281,388,376],{"class":365},[281,390,391],{"class":365}," lint:fix",[281,393,394],{"class":291},"      # привести файлы\n",[244,396,397,398,401,402,405],{},"php-cs-fixer, правила в ",[268,399,400],{},".php-cs-fixer.dist.php",", набор — ",[268,403,404],{},"@PSR12"," целиком.",[244,407,408,409,411,412,415,416,419,420,423,424,427,428,431],{},"Из ",[268,410,270],{}," он НЕ зовётся, и это решение, а не недоделка: сборке хватает\n",[268,413,414],{},"php",", ",[268,417,418],{},"git"," и ",[268,421,422],{},"zip",", и она обязана отрабатывать в свежем клоне. Позови она\nлинтер — ",[268,425,426],{},".\u002Fbuild.sh --check"," перестал бы запускаться, пока не сделан\n",[268,429,430],{},"composer install",", то есть проверка поставки начала бы зависеть от сети.\nПоэтому проверки две, и обе обязательны в CI.",[244,433,434,435,438,439,442,443,446,447,449,450,453,454,457,458,262],{},"Версия инструмента в ",[268,436,437],{},"composer.json"," пришпилена ",[345,440,441],{},"точно",", без ",[268,444,445],{},"^",": набор\n",[268,448,404],{}," у php-cs-fixer пополняется в минорных выпусках, и с ",[268,451,452],{},"^3.0"," CI однажды\nпокраснел бы на коммите, который ничего не менял. Обновление — осознанная\nправка одной строки, следом ",[268,455,456],{},"composer update"," и новый ",[268,459,460],{},"composer.lock",[244,462,463,465,466,469],{},[268,464,460],{}," под контролем git — в отличие от обычая для библиотек, и\nнамеренно. Точная версия пришпиливает сам php-cs-fixer, но не три десятка его\nзависимостей: без lock CI разрешал бы их заново на каждом прогоне. На\nзависимости пакета это не влияет — Composer читает lock только корневого\nпроекта, а в поставку файл не едет (KEEP плюс ",[268,467,468],{},"export-ignore",").",[244,471,472,475,476,479,480,419,483,486,487,490],{},[268,473,474],{},"config.platform.php"," = ",[268,477,478],{},"8.2.0"," — тоже не украшение. Без него Composer\nразрешает зависимости под тот PHP, на котором запущен, и lock, собранный на\n8.4, на нижней границе поддержки не ставится: измерено на CI —\n",[268,481,482],{},"symfony\u002Fstring v8.1.7",[268,484,485],{},"sebastian\u002Fdiff 9.0.1"," требуют 8.4. С пином lock\nгодится для всех версий из матрицы. На потребителей пакета это не влияет:\n",[268,488,489],{},"config"," Composer читает только у корневого проекта.",[244,492,493,496,497,499,500,503,504,507],{},[268,494,495],{},"config.allow-plugins"," в ",[268,498,437],{}," — не украшение: ",[268,501,502],{},"composer\u002Finstallers","\nэто плагин, а Composer с 2.2 по умолчанию блокирует плагины и в неинтерактивном\nрежиме просто падает. Без этого ключа задача ",[268,505,506],{},"Lint"," не доходила даже до\nлинтера.",[244,509,510,511,419,514,517],{},"Чего линтер не ловит: отступы в инлайновом HTML. Строки между ",[268,512,513],{},"?>",[268,515,516],{},"\u003C?php","\nдля него текст, а не код. См. CLAUDE.md, «Линтер не видит отступы в инлайновом\nHTML».",[264,519,521],{"id":520},"что-проверяется","Что проверяется",[523,524,525,538],"table",{},[526,527,528],"thead",{},[529,530,531,535],"tr",{},[532,533,534],"th",{},"проверка",[532,536,537],{},"что ловит",[539,540,541,552,562,575,585,598,611,624,637,651,668,685],"tbody",{},[529,542,543,549],{},[544,545,546],"td",{},[268,547,548],{},"check_filenames",[544,550,551],{},"имя, с которым не справится скрипт; символическую ссылку под контролем git",[529,553,554,559],{},[544,555,556],{},[268,557,558],{},"check_lists",[544,560,561],{},"файл, не попавший ни в SHIP, ни в KEEP",[529,563,564,569],{},[544,565,566],{},[268,567,568],{},"check_gitattributes",[544,570,571,572,574],{},"расхождение KEEP и ",[268,573,468],{}," — в обе стороны",[529,576,577,582],{},[544,578,579],{},[268,580,581],{},"check_encoding",[544,583,584],{},"файл не в UTF-8, BOM в начале файла",[529,586,587,592],{},[544,588,589],{},[268,590,591],{},"check_php",[544,593,594,597],{},[268,595,596],{},"php -l"," по всем PHP",[529,599,600,605],{},[544,601,602],{},[268,603,604],{},"check_short_tags",[544,606,607,608],{},"короткий тег ",[268,609,610],{},"\u003C?",[529,612,613,618],{},[544,614,615],{},[268,616,617],{},"check_js",[544,619,620,623],{},[268,621,622],{},"node --check"," по всем JS",[529,625,626,631],{},[544,627,628],{},[268,629,630],{},"check_lowercase",[544,632,633,634],{},"заглавные буквы в путях ",[268,635,636],{},"lib\u002F",[529,638,639,644],{},[544,640,641],{},[268,642,643],{},"check_changelog_section",[544,645,646,647,650],{},"в ",[268,648,649],{},"CHANGELOG.md"," нет секции текущей версии",[529,652,653,658],{},[544,654,655],{},[268,656,657],{},"check_version",[544,659,660,661,664,665],{},"пустой или кривой ",[268,662,663],{},"VERSION",", пустой ",[268,666,667],{},"VERSION_DATE",[529,669,670,675],{},[544,671,672],{},[268,673,674],{},"run_tests",[544,676,677,680,681,684],{},[268,678,679],{},"tests\u002F*_test.php"," (php) и ",[268,682,683],{},"tests\u002F*_test.mjs"," (node)",[529,686,687,692],{},[544,688,689],{},[268,690,691],{},"check_composer_package",[544,693,694,695,698],{},"состав ",[268,696,697],{},"git archive"," разошёлся со списком SHIP",[244,700,701,702,262],{},"При сборке дополнительно: состав zip сверяется со списком SHIP, а первый\nуровень внутри архива — с ",[268,703,704],{},"shef.options\u002F",[706,707,709],"h3",{"id":708},"что-здесь-сделано-строже-чем-хотелось","Что здесь сделано «строже, чем хотелось»",[244,711,712,715],{},[345,713,714],{},"Пропущенная проверка выглядит как пройденная."," Поэтому:",[717,718,719,731,736],"ul",{},[720,721,722,723,726,727,730],"li",{},"нет ",[268,724,725],{},"node",", а JS или ",[268,728,729],{},"*_test.mjs"," в репозитории есть — отказ, а не примечание;",[720,732,733,735],{},[268,734,674],{}," сначала считает, сколько тестов ЕСТЬ, и сверяет с числом\nпрогнанных: переименованный файл или тест в подкаталоге иначе выпал бы из\nпрогона молча;",[720,737,738,740,741,744,745,748,749,752,753,755],{},[268,739,691],{}," сверяет ",[345,742,743],{},"индекс"," (",[268,746,747],{},"git write-tree","), а не ",[268,750,751],{},"HEAD",", и\nпотому работает и на грязном дереве. Раньше он на ней пропускался — а локально\nдерево грязное почти всегда, так что единственная проверка, ловящая\nрасхождения самого ",[268,754,697],{},", срабатывала только в CI.",[244,757,758,761,762,765,766,768,769,771],{},[345,759,760],{},"Символическая ссылка разводит каналы поставки молча."," ",[268,763,764],{},"cp"," в архив\nразыменовывает её и кладёт содержимое цели, ",[268,767,697],{}," кладёт саму ссылку, а\nобе сверки состава при этом зелёные — они сверяют имена. Поэтому ссылок под\nконтролем git просто не бывает: ",[268,770,548],{}," роняет сборку.",[706,773,775],{"id":774},"про-короткие-теги-отдельно","Про короткие теги отдельно",[244,777,778,780,781,784,785,788,789,791],{},[268,779,596],{}," их ",[345,782,783],{},"не ловит",". При ",[268,786,787],{},"short_open_tag=Off"," — а это значение по\nумолчанию — ",[268,790,610],{}," открывающим тегом не считается, и файл целиком становится\nинлайновым HTML. Синтаксически он остаётся правильным, в нём просто нет PHP.\nА на портале классы из такого файла не определяются, зато исходник уезжает в\nбраузер.",[244,793,794,795,797,798,801,802,804,805,808,809,262],{},"Поэтому ",[268,796,604],{}," спрашивает сам PHP через ",[268,799,800],{},"token_get_all()",", а не\ngrep: ",[268,803,610],{}," внутри строки или комментария лежит в своём токене, опасный же\nостаётся куском ",[268,806,807],{},"T_INLINE_HTML",". Наивный grep краснел бы на каждом регулярном\nвыражении вида ",[268,810,811],{},"\u002F\u003C?\u002F",[264,813,815],{"id":814},"архив","Архив",[244,817,818,819,822,823,825,826,829],{},"Архив содержит ",[345,820,821],{},"каталог модуля целиком",": первым уровнем внутри zip лежит\n",[268,824,704],{},", иначе при распаковке файлы рассыплются прямо по\n",[268,827,828],{},"bitrix\u002Fmodules\u002F",". Проверяется в самом скрипте, а не глазами.",[244,831,832],{},"Сверить поставку двумя путями установки можно так:",[273,834,836],{"className":275,"code":835,"language":277,"meta":105,"style":105},"git archive --format=tar \"$(git write-tree)\" | tar -tf - | grep -v '\u002F$' | sort > \u002Ftmp\u002Fcomposer.txt\n.\u002Fbuild.sh && unzip -Z1 shef.options.zip | grep -v '\u002F$' | sed 's#^shef.options\u002F##' | sort > \u002Ftmp\u002Fzip.txt\ndiff \u002Ftmp\u002Fcomposer.txt \u002Ftmp\u002Fzip.txt    # должно быть пусто\n",[268,837,838,901,951],{"__ignoreMap":105},[281,839,840,842,845,848,852,854,857,860,864,867,870,873,875,878,881,884,887,890,892,895,898],{"class":283,"line":284},[281,841,418],{"class":287},[281,843,844],{"class":365}," archive",[281,846,847],{"class":300}," --format=tar",[281,849,851],{"class":850},"sjJ54"," \"$(",[281,853,418],{"class":287},[281,855,856],{"class":365}," write-tree",[281,858,859],{"class":850},")\"",[281,861,863],{"class":862},"smGrS"," |",[281,865,866],{"class":287}," tar",[281,868,869],{"class":300}," -tf",[281,871,872],{"class":365}," -",[281,874,863],{"class":862},[281,876,877],{"class":287}," grep",[281,879,880],{"class":300}," -v",[281,882,883],{"class":850}," '",[281,885,886],{"class":365},"\u002F$",[281,888,889],{"class":850},"'",[281,891,863],{"class":862},[281,893,894],{"class":287}," sort",[281,896,897],{"class":862}," >",[281,899,900],{"class":365}," \u002Ftmp\u002Fcomposer.txt\n",[281,902,903,905,909,912,915,918,920,922,924,926,928,930,932,935,937,940,942,944,946,948],{"class":283,"line":295},[281,904,288],{"class":287},[281,906,908],{"class":907},"sP7_E"," &&",[281,910,911],{"class":287}," unzip",[281,913,914],{"class":300}," -Z1",[281,916,917],{"class":365}," shef.options.zip",[281,919,863],{"class":862},[281,921,877],{"class":287},[281,923,880],{"class":300},[281,925,883],{"class":850},[281,927,886],{"class":365},[281,929,889],{"class":850},[281,931,863],{"class":862},[281,933,934],{"class":287}," sed",[281,936,883],{"class":850},[281,938,939],{"class":365},"s#^shef.options\u002F##",[281,941,889],{"class":850},[281,943,863],{"class":862},[281,945,894],{"class":287},[281,947,897],{"class":862},[281,949,950],{"class":365}," \u002Ftmp\u002Fzip.txt\n",[281,952,953,956,959,962],{"class":283,"line":307},[281,954,955],{"class":287},"diff",[281,957,958],{"class":365}," \u002Ftmp\u002Fcomposer.txt",[281,960,961],{"class":365}," \u002Ftmp\u002Fzip.txt",[281,963,964],{"class":291},"    # должно быть пусто\n",[264,966,968],{"id":967},"ci","CI",[244,970,971,974],{},[268,972,973],{},".github\u002Fworkflows\u002Fci.yml",", четыре задачи:",[523,976,977,987],{},[526,978,979],{},[529,980,981,984],{},[532,982,983],{},"задача",[532,985,986],{},"что делает",[539,988,989,1007,1019,1033],{},[529,990,991,1000],{},[544,992,993,996,997],{},[268,994,995],{},"PHP 8.2"," … ",[268,998,999],{},"PHP 8.5",[544,1001,1002,415,1004],{},[268,1003,426],{},[268,1005,1006],{},"fail-fast: false",[529,1008,1009,1014],{},[544,1010,1011],{},[268,1012,1013],{},"Build",[544,1015,1016,1018],{},[268,1017,288],{}," плюс архив артефактом прогона",[529,1020,1021,1025],{},[544,1022,1023],{},[268,1024,506],{},[544,1026,1027,419,1029,1032],{},[268,1028,430],{},[268,1030,1031],{},"composer run lint",", одна версия PHP",[529,1034,1035,1039],{},[544,1036,1037],{},[268,1038,968],{},[544,1040,1041,1042],{},"ворота, ",[268,1043,1044],{},"needs: [checks, build, lint]",[244,1046,1047,1049],{},[268,1048,506],{}," гоняется на одной версии PHP, а не на матрице: форматирование от версии\nрантайма не зависит, а четыре одинаковых прогона только тянули бы время.",[244,1051,1052,1053,1055],{},"В защите ветки требуется ровно одна проверка — ",[268,1054,968],{},". Остальные её зависимости,\nпоэтому новая задача не потребует правки ruleset.",[706,1057,1059,1060,1063],{"id":1058},"что-в-ciyml-выглядит-ошибкой-но-ею-не-является","Что в ",[268,1061,1062],{},"ci.yml"," выглядит ошибкой, но ею не является",[244,1065,1066,1074,1075,1079,1080,1083,1084,262],{},[345,1067,1068,1071,1072],{},[268,1069,1070],{},"if: always()"," у задачи ",[268,1073,968],{}," — обязателен вместе с явной сверкой результатов\nзависимостей. Без него задача была бы ",[1076,1077,1078],"em",{},"пропущена"," при падении зависимости, а\nпропущенную проверку защита ветки засчитывает как ",[1076,1081,1082],{},"пройденную",": красный CI уехал\nбы в ",[268,1085,1086],{},"main",[244,1088,1089,1090,1093],{},"Подмывает заменить на ",[268,1091,1092],{},"!cancelled()"," — не надо. Тогда отменённый прогон стал бы\nдавать пропущенную проверку, и, отменив прогон вручную, можно было бы смержить\nнепроверенное.",[244,1095,1096,1103],{},[345,1097,1098,1099,1102],{},"Вытесненный по ",[268,1100,1101],{},"concurrency"," прогон краснеет"," на устаревшем коммите. Это шум,\nа не поломка: защита смотрит на проверки головного коммита.",[264,1105,1107],{"id":1106},"релиз","Релиз",[244,1109,1110,1113],{},[268,1111,1112],{},".github\u002Fworkflows\u002Frelease.yml",", два входа.",[244,1115,1116,1122,1123,1126,1127,1129,1130,1133],{},[345,1117,1118,1119],{},"Пуш тега ",[268,1120,1121],{},"v*"," — тег ",[345,1124,1125],{},"сверяется"," с ",[268,1128,663],{}," из ",[268,1131,1132],{},"install\u002Fversion.php",".\nРасхождение роняет прогон: тегу не доверяем, иначе на портал уедет архив,\nверсия которого врёт.",[244,1135,1136,1122,1144,1129,1147,1149],{},[345,1137,1138,1141,1142],{},[268,1139,1140],{},"workflow_dispatch"," от ",[268,1143,1086],{},[345,1145,1146],{},"выводится",[268,1148,663],{}," и ставится\nсам. Запуск от другой ветки отклоняется, занятый тег ловится до сборки.",[244,1151,1152],{},"Второй вход обязателен: пуш тегов бывает недоступен — другие права, прокси\nсессии, — а релиз выпускать надо.",[244,1154,1155,1158],{},[345,1156,1157],{},"Тег ставится после успешной сборки."," Поставленный раньше, он пережил бы\nупавшую сборку, и следующая попытка упёрлась бы в занятый тег.",[244,1160,1161,1164,1165,1168,1169,1171,1172,1175],{},[345,1162,1163],{},"Примечания к релизу"," собирает ",[268,1166,1167],{},".\u002Fbuild.sh --notes"," — секции ",[268,1170,649],{},"\nот текущей версии до предыдущего ",[345,1173,1174],{},"выпущенного"," тега, не включая его.",[244,1177,1178,1179,1181,1182,419,1185,1188,1189,415,1192,1195,1196,262],{},"Раньше бралась одна секция текущей версии, и это молча теряло всё, что слили\nв ",[268,1180,1086],{},", но не выпустили. Между ",[268,1183,1184],{},"v3.0.6",[268,1186,1187],{},"v3.0.14"," так накопилось семь\nсекций: на странице релиза стоял один линтер, а в архиве лежали ещё и починка\n",[268,1190,1191],{},"toArray()",[268,1193,1194],{},"\\Stringable"," и снятие ",[268,1197,1198],{},"_log1()",[244,1200,1201],{},"Заголовок текущей версии не печатается — он и так стоит заголовком релиза.\nЗаголовки версий, которые отдельным релизом не выходили, наоборот нужны: без\nних бульеты нескольких выпусков слиплись бы в один список. Перед ними встаёт\nстрока «Версии ниже отдельными релизами не выпускались — их изменения в этом\nархиве».",[244,1203,1204,1205,1208,1209,1212,1213,1218,1219,1221,1222,1224,1225,1227,1228,1230,1231,262],{},"Предыдущий выпуск ищется по тегам: самый старший ",[268,1206,1207],{},"vX.Y.Z",", который ",[345,1210,1211],{},"строго\nмладше"," текущей версии и ",[345,1214,1215,1216],{},"достижим из ",[268,1217,751],{},". Оба условия про одно и то\nже — не начать отсчёт от версии, секции которой в этом ",[268,1220,649],{}," нет.\nТег выше текущей версии так и выглядит; тег на ветке поддержки или\nпоставленный руками мимо ",[268,1223,1086],{}," — тоже, только снизу. Сравниваются числа по\nтрём частям, а не строки: строкой «3.0.9» больше «3.0.14», а ",[268,1226,418],{}," и вовсе\nпечатает ",[268,1229,1187],{}," раньше ",[268,1232,1233],{},"v3.0.2",[706,1235,1237],{"id":1236},"когда-граница-не-определилась","Когда граница не определилась",[244,1239,1240,1241,1244,1245,1248],{},"Три случая, и все три ведут себя одинаково: примечания идут ",[345,1242,1243],{},"до конца\nфайла",", а строка «Версии ниже отдельными релизами не выпускались» ",[345,1246,1247],{},"не\nпечатается"," — под ней оказались бы выпущенные версии, и страница релиза\nутверждала бы неправду ровно тогда, когда что-то пошло не так.",[523,1250,1251,1261],{},[526,1252,1253],{},[529,1254,1255,1258],{},[532,1256,1257],{},"что случилось",[532,1259,1260],{},"что в stderr",[539,1262,1263,1274,1285],{},[529,1264,1265,1271],{},[544,1266,1267,1268,1270],{},"тегов ",[268,1269,1121],{}," нет вовсе — первый выпуск или клон без тегов",[544,1272,1273],{},"«Предыдущих тегов нет»",[529,1275,1276,1282],{},[544,1277,1278,1279,1281],{},"секции предыдущего выпуска в ",[268,1280,649],{}," нет",[544,1283,1284],{},"«нет секции X»",[529,1286,1287,1290],{},[544,1288,1289],{},"секция предыдущего выпуска стоит не ниже текущей",[544,1291,1292],{},"«стоит не ниже»",[244,1294,1295,1296,1299],{},"Третий случай — это и ",[268,1297,1298],{},".\u002Fbuild.sh --notes \u003Cтекущая версия>",": спутать легко, а\nмолча уехала бы вся история модуля.",[244,1301,1302,1303,1306,1307,496,1309,1311],{},"Обратного случая — секции ",[345,1304,1305],{},"текущей"," версии нет, примечания пустые — ждать\nне надо: его ловит ",[268,1308,643],{},[268,1310,426],{},", то есть\nв PR, а не в момент выпуска.",[244,1313,1314,1315,1317,1318,1321],{},"Логика лежит в ",[268,1316,270],{},", а не в теле workflow, по одной причине: то же\nсамое получается локально одной командой, и на неё написан тест\n(",[268,1319,1320],{},"tests\u002Frelease_notes_test.php","). Код в yaml не проверяется ничем, кроме\nвыпуска релиза.",[244,1323,1324,1325,1327],{},"Выбранный предыдущий выпуск печатается в stderr — в журнале релиза видно, от\nчего шёл отсчёт. Локально перед прогоном нужны свежие теги: в клоне, где их\nнет совсем, в примечания уйдёт весь ",[268,1326,649],{},", а где они протухли —\nотсчёт пойдёт от последнего известного, и примечаний окажется больше, чем\nнадо.",[273,1329,1331],{"className":275,"code":1330,"language":277,"meta":105,"style":105},"git fetch --tags origin && .\u002Fbuild.sh --notes\n",[268,1332,1333],{"__ignoreMap":105},[281,1334,1335,1337,1340,1343,1346,1348,1351],{"class":283,"line":284},[281,1336,418],{"class":287},[281,1338,1339],{"class":365}," fetch",[281,1341,1342],{"class":300}," --tags",[281,1344,1345],{"class":365}," origin",[281,1347,908],{"class":907},[281,1349,1350],{"class":287}," .\u002Fbuild.sh",[281,1352,1353],{"class":300}," --notes\n",[244,1355,1356,1359],{},[345,1357,1358],{},"Собрать примечания к уже выпущенному релизу"," — тем же режимом, аргументом.\nТак восстанавливается текст для выпуска, который вышел с неполными\nпримечаниями:",[273,1361,1363],{"className":275,"code":1362,"language":277,"meta":105,"style":105},"mkdir -p \u002Ftmp\u002Fnotes\u002Finstall && cp build.sh \u002Ftmp\u002Fnotes\u002F\ngit show v3.0.14:CHANGELOG.md      > \u002Ftmp\u002Fnotes\u002FCHANGELOG.md\ngit show v3.0.14:install\u002Fversion.php > \u002Ftmp\u002Fnotes\u002Finstall\u002Fversion.php\n\u002Ftmp\u002Fnotes\u002Fbuild.sh --notes 3.0.6\n",[268,1364,1365,1387,1403,1417],{"__ignoreMap":105},[281,1366,1367,1370,1373,1376,1378,1381,1384],{"class":283,"line":284},[281,1368,1369],{"class":287},"mkdir",[281,1371,1372],{"class":300}," -p",[281,1374,1375],{"class":365}," \u002Ftmp\u002Fnotes\u002Finstall",[281,1377,908],{"class":907},[281,1379,1380],{"class":287}," cp",[281,1382,1383],{"class":365}," build.sh",[281,1385,1386],{"class":365}," \u002Ftmp\u002Fnotes\u002F\n",[281,1388,1389,1391,1394,1397,1400],{"class":283,"line":295},[281,1390,418],{"class":287},[281,1392,1393],{"class":365}," show",[281,1395,1396],{"class":365}," v3.0.14:CHANGELOG.md",[281,1398,1399],{"class":862},"      >",[281,1401,1402],{"class":365}," \u002Ftmp\u002Fnotes\u002FCHANGELOG.md\n",[281,1404,1405,1407,1409,1412,1414],{"class":283,"line":307},[281,1406,418],{"class":287},[281,1408,1393],{"class":365},[281,1410,1411],{"class":365}," v3.0.14:install\u002Fversion.php",[281,1413,897],{"class":862},[281,1415,1416],{"class":365}," \u002Ftmp\u002Fnotes\u002Finstall\u002Fversion.php\n",[281,1418,1419,1422,1424],{"class":283,"line":318},[281,1420,1421],{"class":287},"\u002Ftmp\u002Fnotes\u002Fbuild.sh",[281,1423,323],{"class":300},[281,1425,1426],{"class":336}," 3.0.6\n",[244,1428,1429,1430,262],{},"Пустые примечания — не ошибка: workflow подставит «Версия X. Изменения — в\nCHANGELOG.md». Ненулевой код возврата уронил бы выпуск из-за оформления\n",[268,1431,649],{},[706,1433,1435],{"id":1434},"packagist","Packagist",[244,1437,1438,1439,1442,1443,419,1446,1449,1450,1453],{},"Последним шагом релиз дёргает ",[268,1440,1441],{},"update-package",". Без секретов\n",[268,1444,1445],{},"PACKAGIST_USERNAME",[268,1447,1448],{},"PACKAGIST_TOKEN"," шаг пропускается, и релиз при этом\n",[345,1451,1452],{},"не падает",": невыложенный релиз чинить нечем, а отставший Packagist\nдогоняется кнопкой Update за десять секунд.",[244,1455,1456,1457,1460,1461,262],{},"Эндпойнт умеет только ",[345,1458,1459],{},"обновлять уже зарегистрированный"," пакет. Первую\nрегистрацию делают один раз руками: packagist.org → Submit →\n",[268,1462,1463],{},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions",[264,1465,1467],{"id":1466},"куда-composer-кладёт-модуль","Куда Composer кладёт модуль",[244,1469,1470,1472,1473,475,1476,1479,1480,1483,1484,1487,1488,1491,1492,1495,1496,1498],{},[268,1471,437],{},": ",[268,1474,1475],{},"type",[268,1477,1478],{},"bitrix-module"," плюс\n",[268,1481,1482],{},"extra.installer-name = shef.options",". Тогда Composer разворачивает модуль в\n",[268,1485,1486],{},"bitrix\u002Fmodules\u002Fshef.options\u002F"," без настройки на стороне потребителя:\n",[268,1489,1490],{},"installer-name"," читается из пакета, а ",[268,1493,1494],{},"{$bitrix_dir}"," — только из корневого\n",[268,1497,437],{},", повлиять на него пакет не может.",[244,1500,1501,1507,1508,1510],{},[345,1502,1503,1506],{},[268,1504,1505],{},"bitrix-d7-module"," развернул бы модуль не туда."," Шаблоны в\n",[268,1509,502],{},":",[717,1512,1513,1521],{},[720,1514,1515,1517,1518],{},[268,1516,1478],{}," → ",[268,1519,1520],{},"{$bitrix_dir}\u002Fmodules\u002F{$name}\u002F",[720,1522,1523,1517,1525],{},[268,1524,1505],{},[268,1526,1527],{},"{$bitrix_dir}\u002Fmodules\u002F{$vendor}.{$name}\u002F",[244,1529,1530,1531,1533,1534,1537,1538,1541,1542,1545],{},"а ",[268,1532,1490],{}," подменяет только ",[268,1535,1536],{},"{$name}",". Для пакета ",[268,1539,1540],{},"bxshef\u002Foptions","\nвторой вариант дал бы ",[268,1543,1544],{},"bitrix\u002Fmodules\u002Fbxshef.shef.options\u002F"," — каталог, которого\nБитрикс не знает.",[244,1547,1548,1549,1510],{},"На стороне проекта-потребителя Composer 2.2+ требует явного разрешения\nплагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в\n",[268,1550,1551],{},"vendor\u002Fbxshef\u002Foptions",[273,1553,1557],{"className":1554,"code":1555,"language":1556,"meta":105,"style":105},"language-json shiki shiki-themes material-theme-lighter github-light github-dark","{\n    \"config\": {\n        \"allow-plugins\": {\n            \"composer\u002Finstallers\": true\n        }\n    }\n}\n","json",[268,1558,1559,1564,1581,1596,1610,1615,1621],{"__ignoreMap":105},[281,1560,1561],{"class":283,"line":284},[281,1562,1563],{"class":907},"{\n",[281,1565,1566,1570,1573,1576,1578],{"class":283,"line":295},[281,1567,1569],{"class":1568},"s39Yj","    \"",[281,1571,489],{"class":1572},"sseR_",[281,1574,1575],{"class":1568},"\"",[281,1577,1510],{"class":907},[281,1579,1580],{"class":907}," {\n",[281,1582,1583,1586,1590,1592,1594],{"class":283,"line":307},[281,1584,1585],{"class":1568},"        \"",[281,1587,1589],{"class":1588},"sZMiF","allow-plugins",[281,1591,1575],{"class":1568},[281,1593,1510],{"class":907},[281,1595,1580],{"class":907},[281,1597,1598,1601,1603,1605,1607],{"class":283,"line":318},[281,1599,1600],{"class":1568},"            \"",[281,1602,502],{"class":336},[281,1604,1575],{"class":1568},[281,1606,1510],{"class":907},[281,1608,1609],{"class":1568}," true\n",[281,1611,1612],{"class":283,"line":329},[281,1613,1614],{"class":907},"        }\n",[281,1616,1618],{"class":283,"line":1617},6,[281,1619,1620],{"class":907},"    }\n",[281,1622,1624],{"class":283,"line":1623},7,[281,1625,1626],{"class":907},"}\n",[244,1628,1629,1631,1632,1634,1635,1638,1639,1642,1643,1646],{},[268,1630,1478],{}," помечен в исходниках ",[268,1633,502],{}," как ",[268,1636,1637],{},"deprecated, remove on the major release",", поэтому в ",[268,1640,1641],{},"require"," стоит потолок\n",[268,1644,1645],{},"\"composer\u002Finstallers\": \"^1.0 || ^2.0\"",". Снимут потолок — модуль уедет в чужой\nкаталог.",[264,1648,92],{"id":1649},"проверка-на-портале",[244,1651,1652,1653,1656,1657,1660,1661,419,1664,262],{},"Каталог модуля браузеру недоступен: в поставке nginx стоит ",[268,1654,1655],{},"deny all"," на\n",[268,1658,1659],{},"^\u002Fbitrix\u002F(modules|local_cache|stack_cache|managed_cache|php_interface)",".\nПоэтому фронт и раскладывается в ",[268,1662,1663],{},"\u002Fbitrix\u002Fcss",[268,1665,1666],{},"\u002Fbitrix\u002Fjs",[244,1668,1669],{},"Проверить на стенде:",[273,1671,1676],{"className":1672,"code":1674,"language":1675},[1673],"language-text","\u002Fbitrix\u002Fmodules\u002Fshef.options\u002Fjs\u002F...        -> 403\n\u002Fbitrix\u002Fcss\u002Fshef.options\u002Fadmin-options.css -> 200\n","text",[268,1677,1674],{"__ignoreMap":105},[244,1679,1680,1681,1684],{},"Полная процедура проверки на портале — в ",[248,1682,1683],{"href":93},"portal-check.md",":\nдесять шагов с ожидаемым результатом, отдельно обновление с 2.x и запуск\nпримеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта\nпроцедура и есть тест.",[1686,1687,1688],"note",{},[244,1689,1690,1691,1696],{},"Источник: ",[248,1692,1695],{"href":1693,"rel":1694},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions\u002Fblob\u002Fmain\u002Fdocs\u002Fbuild-and-install.md",[256],"options\u002Fdocs\u002Fbuild-and-install.md"," — правки туда, сайт пересобирается сам.",[1698,1699,1700],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sseR_, html code.shiki .sseR_{--shiki-light:#9C3EDA;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":105,"searchDepth":307,"depth":307,"links":1702},[1703,1705,1706,1710,1711,1715,1719,1720],{"id":266,"depth":295,"text":1704},"build.sh — единственная точка входа сборки",{"id":351,"depth":295,"text":352},{"id":520,"depth":295,"text":521,"children":1707},[1708,1709],{"id":708,"depth":307,"text":709},{"id":774,"depth":307,"text":775},{"id":814,"depth":295,"text":815},{"id":967,"depth":295,"text":968,"children":1712},[1713],{"id":1058,"depth":307,"text":1714},"Что в ci.yml выглядит ошибкой, но ею не является",{"id":1106,"depth":295,"text":1107,"children":1716},[1717,1718],{"id":1236,"depth":307,"text":1237},{"id":1434,"depth":307,"text":1435},{"id":1466,"depth":295,"text":1467},{"id":1649,"depth":295,"text":92},"md",{},true,"---\ntitle: \"Сборка, CI и релиз\"\n---\n\nРаскладка репозитория — в [module-structure.md](\u002Fmodules\u002Foptions\u002Fmodule-structure), процесс —\nв [CONTRIBUTING.md](https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions\u002Fblob\u002Fmain\u002FCONTRIBUTING.md), установка глазами пользователя —\nв [README.md](\u002Fmodules\u002Foptions).\n\n## `build.sh` — единственная точка входа сборки\n\n```bash\n.\u002Fbuild.sh            # проверки + архив shef.options.zip\n.\u002Fbuild.sh --check    # только проверки\n.\u002Fbuild.sh --version  # напечатать версию модуля\n.\u002Fbuild.sh --notes    # примечания к релизу из CHANGELOG\n.\u002Fbuild.sh --notes 3.0.6   # то же, но отсчёт от указанного выпуска\n```\n\nCI зовёт **её же**. Это не украшение: если бы сервер гонял свой набор команд,\nлокальный зелёный прогон и серверный красный означали бы разные вещи, и\nразбираться пришлось бы в двух местах сразу.\n\n## Линтер — рядом, а не внутри\n\n```bash\ncomposer install           # поднять инструменты разработчика\ncomposer run lint          # сухой прогон: покажет диф и упадёт\ncomposer run lint:fix      # привести файлы\n```\n\nphp-cs-fixer, правила в `.php-cs-fixer.dist.php`, набор — `@PSR12` целиком.\n\nИз `build.sh` он НЕ зовётся, и это решение, а не недоделка: сборке хватает\n`php`, `git` и `zip`, и она обязана отрабатывать в свежем клоне. Позови она\nлинтер — `.\u002Fbuild.sh --check` перестал бы запускаться, пока не сделан\n`composer install`, то есть проверка поставки начала бы зависеть от сети.\nПоэтому проверки две, и обе обязательны в CI.\n\nВерсия инструмента в `composer.json` пришпилена **точно**, без `^`: набор\n`@PSR12` у php-cs-fixer пополняется в минорных выпусках, и с `^3.0` CI однажды\nпокраснел бы на коммите, который ничего не менял. Обновление — осознанная\nправка одной строки, следом `composer update` и новый `composer.lock`.\n\n`composer.lock` под контролем git — в отличие от обычая для библиотек, и\nнамеренно. Точная версия пришпиливает сам php-cs-fixer, но не три десятка его\nзависимостей: без lock CI разрешал бы их заново на каждом прогоне. На\nзависимости пакета это не влияет — Composer читает lock только корневого\nпроекта, а в поставку файл не едет (KEEP плюс `export-ignore`).\n\n`config.platform.php` = `8.2.0` — тоже не украшение. Без него Composer\nразрешает зависимости под тот PHP, на котором запущен, и lock, собранный на\n8.4, на нижней границе поддержки не ставится: измерено на CI —\n`symfony\u002Fstring v8.1.7` и `sebastian\u002Fdiff 9.0.1` требуют 8.4. С пином lock\nгодится для всех версий из матрицы. На потребителей пакета это не влияет:\n`config` Composer читает только у корневого проекта.\n\n`config.allow-plugins` в `composer.json` — не украшение: `composer\u002Finstallers`\nэто плагин, а Composer с 2.2 по умолчанию блокирует плагины и в неинтерактивном\nрежиме просто падает. Без этого ключа задача `Lint` не доходила даже до\nлинтера.\n\nЧего линтер не ловит: отступы в инлайновом HTML. Строки между `?>` и `\u003C?php`\nдля него текст, а не код. См. CLAUDE.md, «Линтер не видит отступы в инлайновом\nHTML».\n\n## Что проверяется\n\n| проверка | что ловит |\n|---|---|\n| `check_filenames` | имя, с которым не справится скрипт; символическую ссылку под контролем git |\n| `check_lists` | файл, не попавший ни в SHIP, ни в KEEP |\n| `check_gitattributes` | расхождение KEEP и `export-ignore` — в обе стороны |\n| `check_encoding` | файл не в UTF-8, BOM в начале файла |\n| `check_php` | `php -l` по всем PHP |\n| `check_short_tags` | короткий тег `\u003C?` |\n| `check_js` | `node --check` по всем JS |\n| `check_lowercase` | заглавные буквы в путях `lib\u002F` |\n| `check_changelog_section` | в `CHANGELOG.md` нет секции текущей версии |\n| `check_version` | пустой или кривой `VERSION`, пустой `VERSION_DATE` |\n| `run_tests` | `tests\u002F*_test.php` (php) и `tests\u002F*_test.mjs` (node) |\n| `check_composer_package` | состав `git archive` разошёлся со списком SHIP |\n\nПри сборке дополнительно: состав zip сверяется со списком SHIP, а первый\nуровень внутри архива — с `shef.options\u002F`.\n\n### Что здесь сделано «строже, чем хотелось»\n\n**Пропущенная проверка выглядит как пройденная.** Поэтому:\n\n* нет `node`, а JS или `*_test.mjs` в репозитории есть — отказ, а не примечание;\n* `run_tests` сначала считает, сколько тестов ЕСТЬ, и сверяет с числом\n  прогнанных: переименованный файл или тест в подкаталоге иначе выпал бы из\n  прогона молча;\n* `check_composer_package` сверяет **индекс** (`git write-tree`), а не `HEAD`, и\n  потому работает и на грязном дереве. Раньше он на ней пропускался — а локально\n  дерево грязное почти всегда, так что единственная проверка, ловящая\n  расхождения самого `git archive`, срабатывала только в CI.\n\n**Символическая ссылка разводит каналы поставки молча.** `cp` в архив\nразыменовывает её и кладёт содержимое цели, `git archive` кладёт саму ссылку, а\nобе сверки состава при этом зелёные — они сверяют имена. Поэтому ссылок под\nконтролем git просто не бывает: `check_filenames` роняет сборку.\n\n### Про короткие теги отдельно\n\n`php -l` их **не ловит**. При `short_open_tag=Off` — а это значение по\nумолчанию — `\u003C?` открывающим тегом не считается, и файл целиком становится\nинлайновым HTML. Синтаксически он остаётся правильным, в нём просто нет PHP.\nА на портале классы из такого файла не определяются, зато исходник уезжает в\nбраузер.\n\nПоэтому `check_short_tags` спрашивает сам PHP через `token_get_all()`, а не\ngrep: `\u003C?` внутри строки или комментария лежит в своём токене, опасный же\nостаётся куском `T_INLINE_HTML`. Наивный grep краснел бы на каждом регулярном\nвыражении вида `\u002F\u003C?\u002F`.\n\n## Архив\n\nАрхив содержит **каталог модуля целиком**: первым уровнем внутри zip лежит\n`shef.options\u002F`, иначе при распаковке файлы рассыплются прямо по\n`bitrix\u002Fmodules\u002F`. Проверяется в самом скрипте, а не глазами.\n\nСверить поставку двумя путями установки можно так:\n\n```bash\ngit archive --format=tar \"$(git write-tree)\" | tar -tf - | grep -v '\u002F$' | sort > \u002Ftmp\u002Fcomposer.txt\n.\u002Fbuild.sh && unzip -Z1 shef.options.zip | grep -v '\u002F$' | sed 's#^shef.options\u002F##' | sort > \u002Ftmp\u002Fzip.txt\ndiff \u002Ftmp\u002Fcomposer.txt \u002Ftmp\u002Fzip.txt    # должно быть пусто\n```\n\n## CI\n\n`.github\u002Fworkflows\u002Fci.yml`, четыре задачи:\n\n| задача | что делает |\n|---|---|\n| `PHP 8.2` … `PHP 8.5` | `.\u002Fbuild.sh --check`, `fail-fast: false` |\n| `Build` | `.\u002Fbuild.sh` плюс архив артефактом прогона |\n| `Lint` | `composer install` и `composer run lint`, одна версия PHP |\n| `CI` | ворота, `needs: [checks, build, lint]` |\n\n`Lint` гоняется на одной версии PHP, а не на матрице: форматирование от версии\nрантайма не зависит, а четыре одинаковых прогона только тянули бы время.\n\nВ защите ветки требуется ровно одна проверка — `CI`. Остальные её зависимости,\nпоэтому новая задача не потребует правки ruleset.\n\n### Что в `ci.yml` выглядит ошибкой, но ею не является\n\n**`if: always()` у задачи `CI`** — обязателен вместе с явной сверкой результатов\nзависимостей. Без него задача была бы *пропущена* при падении зависимости, а\nпропущенную проверку защита ветки засчитывает как *пройденную*: красный CI уехал\nбы в `main`.\n\nПодмывает заменить на `!cancelled()` — не надо. Тогда отменённый прогон стал бы\nдавать пропущенную проверку, и, отменив прогон вручную, можно было бы смержить\nнепроверенное.\n\n**Вытесненный по `concurrency` прогон краснеет** на устаревшем коммите. Это шум,\nа не поломка: защита смотрит на проверки головного коммита.\n\n## Релиз\n\n`.github\u002Fworkflows\u002Frelease.yml`, два входа.\n\n**Пуш тега `v*`** — тег **сверяется** с `VERSION` из `install\u002Fversion.php`.\nРасхождение роняет прогон: тегу не доверяем, иначе на портал уедет архив,\nверсия которого врёт.\n\n**`workflow_dispatch` от `main`** — тег **выводится** из `VERSION` и ставится\nсам. Запуск от другой ветки отклоняется, занятый тег ловится до сборки.\n\nВторой вход обязателен: пуш тегов бывает недоступен — другие права, прокси\nсессии, — а релиз выпускать надо.\n\n**Тег ставится после успешной сборки.** Поставленный раньше, он пережил бы\nупавшую сборку, и следующая попытка упёрлась бы в занятый тег.\n\n**Примечания к релизу** собирает `.\u002Fbuild.sh --notes` — секции `CHANGELOG.md`\nот текущей версии до предыдущего **выпущенного** тега, не включая его.\n\nРаньше бралась одна секция текущей версии, и это молча теряло всё, что слили\nв `main`, но не выпустили. Между `v3.0.6` и `v3.0.14` так накопилось семь\nсекций: на странице релиза стоял один линтер, а в архиве лежали ещё и починка\n`toArray()`, `\\Stringable` и снятие `_log1()`.\n\nЗаголовок текущей версии не печатается — он и так стоит заголовком релиза.\nЗаголовки версий, которые отдельным релизом не выходили, наоборот нужны: без\nних бульеты нескольких выпусков слиплись бы в один список. Перед ними встаёт\nстрока «Версии ниже отдельными релизами не выпускались — их изменения в этом\nархиве».\n\nПредыдущий выпуск ищется по тегам: самый старший `vX.Y.Z`, который **строго\nмладше** текущей версии и **достижим из `HEAD`**. Оба условия про одно и то\nже — не начать отсчёт от версии, секции которой в этом `CHANGELOG.md` нет.\nТег выше текущей версии так и выглядит; тег на ветке поддержки или\nпоставленный руками мимо `main` — тоже, только снизу. Сравниваются числа по\nтрём частям, а не строки: строкой «3.0.9» больше «3.0.14», а `git` и вовсе\nпечатает `v3.0.14` раньше `v3.0.2`.\n\n### Когда граница не определилась\n\nТри случая, и все три ведут себя одинаково: примечания идут **до конца\nфайла**, а строка «Версии ниже отдельными релизами не выпускались» **не\nпечатается** — под ней оказались бы выпущенные версии, и страница релиза\nутверждала бы неправду ровно тогда, когда что-то пошло не так.\n\n| что случилось | что в stderr |\n|---|---|\n| тегов `v*` нет вовсе — первый выпуск или клон без тегов | «Предыдущих тегов нет» |\n| секции предыдущего выпуска в `CHANGELOG.md` нет | «нет секции X» |\n| секция предыдущего выпуска стоит не ниже текущей | «стоит не ниже» |\n\nТретий случай — это и `.\u002Fbuild.sh --notes \u003Cтекущая версия>`: спутать легко, а\nмолча уехала бы вся история модуля.\n\nОбратного случая — секции **текущей** версии нет, примечания пустые — ждать\nне надо: его ловит `check_changelog_section` в `.\u002Fbuild.sh --check`, то есть\nв PR, а не в момент выпуска.\n\nЛогика лежит в `build.sh`, а не в теле workflow, по одной причине: то же\nсамое получается локально одной командой, и на неё написан тест\n(`tests\u002Frelease_notes_test.php`). Код в yaml не проверяется ничем, кроме\nвыпуска релиза.\n\nВыбранный предыдущий выпуск печатается в stderr — в журнале релиза видно, от\nчего шёл отсчёт. Локально перед прогоном нужны свежие теги: в клоне, где их\nнет совсем, в примечания уйдёт весь `CHANGELOG.md`, а где они протухли —\nотсчёт пойдёт от последнего известного, и примечаний окажется больше, чем\nнадо.\n\n```bash\ngit fetch --tags origin && .\u002Fbuild.sh --notes\n```\n\n**Собрать примечания к уже выпущенному релизу** — тем же режимом, аргументом.\nТак восстанавливается текст для выпуска, который вышел с неполными\nпримечаниями:\n\n```bash\nmkdir -p \u002Ftmp\u002Fnotes\u002Finstall && cp build.sh \u002Ftmp\u002Fnotes\u002F\ngit show v3.0.14:CHANGELOG.md      > \u002Ftmp\u002Fnotes\u002FCHANGELOG.md\ngit show v3.0.14:install\u002Fversion.php > \u002Ftmp\u002Fnotes\u002Finstall\u002Fversion.php\n\u002Ftmp\u002Fnotes\u002Fbuild.sh --notes 3.0.6\n```\n\nПустые примечания — не ошибка: workflow подставит «Версия X. Изменения — в\nCHANGELOG.md». Ненулевой код возврата уронил бы выпуск из-за оформления\n`CHANGELOG.md`.\n\n### Packagist\n\nПоследним шагом релиз дёргает `update-package`. Без секретов\n`PACKAGIST_USERNAME` и `PACKAGIST_TOKEN` шаг пропускается, и релиз при этом\n**не падает**: невыложенный релиз чинить нечем, а отставший Packagist\nдогоняется кнопкой Update за десять секунд.\n\nЭндпойнт умеет только **обновлять уже зарегистрированный** пакет. Первую\nрегистрацию делают один раз руками: packagist.org → Submit →\n`https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions`.\n\n## Куда Composer кладёт модуль\n\n`composer.json`: `type` = `bitrix-module` плюс\n`extra.installer-name = shef.options`. Тогда Composer разворачивает модуль в\n`bitrix\u002Fmodules\u002Fshef.options\u002F` без настройки на стороне потребителя:\n`installer-name` читается из пакета, а `{$bitrix_dir}` — только из корневого\n`composer.json`, повлиять на него пакет не может.\n\n**`bitrix-d7-module` развернул бы модуль не туда.** Шаблоны в\n`composer\u002Finstallers`:\n\n* `bitrix-module` → `{$bitrix_dir}\u002Fmodules\u002F{$name}\u002F`\n* `bitrix-d7-module` → `{$bitrix_dir}\u002Fmodules\u002F{$vendor}.{$name}\u002F`\n\nа `installer-name` подменяет только `{$name}`. Для пакета `bxshef\u002Foptions`\nвторой вариант дал бы `bitrix\u002Fmodules\u002Fbxshef.shef.options\u002F` — каталог, которого\nБитрикс не знает.\n\nНа стороне проекта-потребителя Composer 2.2+ требует явного разрешения\nплагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в\n`vendor\u002Fbxshef\u002Foptions`:\n\n```json\n{\n\t\"config\": {\n\t\t\"allow-plugins\": {\n\t\t\t\"composer\u002Finstallers\": true\n\t\t}\n\t}\n}\n```\n\n`bitrix-module` помечен в исходниках `composer\u002Finstallers` как `deprecated,\nremove on the major release`, поэтому в `require` стоит потолок\n`\"composer\u002Finstallers\": \"^1.0 || ^2.0\"`. Снимут потолок — модуль уедет в чужой\nкаталог.\n\n## Проверка на портале\n\nКаталог модуля браузеру недоступен: в поставке nginx стоит `deny all` на\n`^\u002Fbitrix\u002F(modules|local_cache|stack_cache|managed_cache|php_interface)`.\nПоэтому фронт и раскладывается в `\u002Fbitrix\u002Fcss` и `\u002Fbitrix\u002Fjs`.\n\nПроверить на стенде:\n\n```\n\u002Fbitrix\u002Fmodules\u002Fshef.options\u002Fjs\u002F...        -> 403\n\u002Fbitrix\u002Fcss\u002Fshef.options\u002Fadmin-options.css -> 200\n```\n\nПолная процедура проверки на портале — в [portal-check.md](\u002Fmodules\u002Foptions\u002Fportal-check):\nдесять шагов с ожидаемым результатом, отдельно обновление с 2.x и запуск\nпримеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта\nпроцедура и есть тест.\n\n::note\nИсточник: [options\u002Fdocs\u002Fbuild-and-install.md](https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Foptions\u002Fblob\u002Fmain\u002Fdocs\u002Fbuild-and-install.md) — правки туда, сайт пересобирается сам.\n::\n\n",{"title":82,"description":85},"BZJVtOHYd4hVZCjwrRA1puVn-iKRVF4y1kIDeExR_8E",[1728,1729],{"title":77,"path":78,"stem":79,"description":80,"children":-1},{"title":87,"path":88,"stem":89,"description":90,"children":-1},1790857269638]