[{"data":1,"prerenderedAt":1595},["ShallowReactive",2],{"navigation_docs":3,"-modules-insync-build-and-install":238,"-modules-insync-build-and-install-surround":1593},[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":1587,"meta":1588,"navigation":1589,"path":236,"rawbody":1590,"seo":1591,"stem":237,"__hash__":1592},"docs\u002F3.modules\u002F3.insync\u002F9.build-and-install.md",{"type":241,"value":242,"toc":1568},"minimark",[243,263,272,316,324,328,492,498,503,509,549,565,569,585,605,609,623,626,760,764,770,866,878,891,897,905,928,935,945,949,955,975,991,994,1000,1010,1014,1033,1043,1047,1069,1130,1142,1160,1163,1326,1330,1362,1375,1392,1410,1416,1491,1511,1514,1528,1531,1539,1545,1552,1564],[244,245,246,247,251,252,258,259,262],"p",{},"Раскладка репозитория — в ",[248,249,250],"a",{"href":192},"module-structure.md",", процесс —\nв ",[248,253,257],{"href":254,"rel":255},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Finsync\u002Fblob\u002Fmain\u002FCONTRIBUTING.md",[256],"nofollow","CONTRIBUTING.md",", установка глазами пользователя —\nв ",[248,260,261],{"href":181},"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.insync.zip\n.\u002Fbuild.sh --check    # только проверки\n.\u002Fbuild.sh --version  # напечатать версию модуля\n","bash",[268,279,280,293,305],{"__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.insync.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",[244,317,318,319,323],{},"CI зовёт ",[320,321,322],"strong",{},"её же",". Это не украшение: если бы сервер гонял свой набор команд,\nлокальный зелёный прогон и серверный красный означали бы разные вещи, и\nразбираться пришлось бы в двух местах сразу.",[264,325,327],{"id":326},"что-проверяется","Что проверяется",[329,330,331,344],"table",{},[332,333,334],"thead",{},[335,336,337,341],"tr",{},[338,339,340],"th",{},"проверка",[338,342,343],{},"что ловит",[345,346,347,358,368,382,392,405,418,431,444,461,478],"tbody",{},[335,348,349,355],{},[350,351,352],"td",{},[268,353,354],{},"check_filenames",[350,356,357],{},"имя, с которым не справится скрипт; символическую ссылку под контролем git",[335,359,360,365],{},[350,361,362],{},[268,363,364],{},"check_lists",[350,366,367],{},"файл, не попавший ни в SHIP, ни в KEEP",[335,369,370,375],{},[350,371,372],{},[268,373,374],{},"check_gitattributes",[350,376,377,378,381],{},"расхождение KEEP и ",[268,379,380],{},"export-ignore"," — в обе стороны",[335,383,384,389],{},[350,385,386],{},[268,387,388],{},"check_encoding",[350,390,391],{},"файл не в UTF-8, BOM в начале файла",[335,393,394,399],{},[350,395,396],{},[268,397,398],{},"check_php",[350,400,401,404],{},[268,402,403],{},"php -l"," по всем PHP",[335,406,407,412],{},[350,408,409],{},[268,410,411],{},"check_short_tags",[350,413,414,415],{},"короткий тег ",[268,416,417],{},"\u003C?",[335,419,420,425],{},[350,421,422],{},[268,423,424],{},"check_js",[350,426,427,430],{},[268,428,429],{},"node --check"," по всем JS",[335,432,433,438],{},[350,434,435],{},[268,436,437],{},"check_lowercase",[350,439,440,441],{},"заглавные буквы в путях ",[268,442,443],{},"lib\u002F",[335,445,446,451],{},[350,447,448],{},[268,449,450],{},"check_version",[350,452,453,454,457,458],{},"пустой или кривой ",[268,455,456],{},"VERSION",", пустой ",[268,459,460],{},"VERSION_DATE",[335,462,463,468],{},[350,464,465],{},[268,466,467],{},"run_tests",[350,469,470,473,474,477],{},[268,471,472],{},"tests\u002F*_test.php"," (php) и ",[268,475,476],{},"tests\u002F*_test.mjs"," (node)",[335,479,480,485],{},[350,481,482],{},[268,483,484],{},"check_composer_package",[350,486,487,488,491],{},"состав ",[268,489,490],{},"git archive"," разошёлся со списком SHIP",[244,493,494,495,262],{},"При сборке дополнительно: состав zip сверяется со списком SHIP, а первый\nуровень внутри архива — с ",[268,496,497],{},"shef.insync\u002F",[499,500,502],"h3",{"id":501},"что-здесь-сделано-строже-чем-хотелось","Что здесь сделано «строже, чем хотелось»",[244,504,505,508],{},[320,506,507],{},"Пропущенная проверка выглядит как пройденная."," Поэтому:",[510,511,512,524,529],"ul",{},[513,514,515,516,519,520,523],"li",{},"нет ",[268,517,518],{},"node",", а JS или ",[268,521,522],{},"*_test.mjs"," в репозитории есть — отказ, а не примечание;",[513,525,526,528],{},[268,527,467],{}," сначала считает, сколько тестов ЕСТЬ, и сверяет с числом\nпрогнанных: переименованный файл или тест в подкаталоге иначе выпал бы из\nпрогона молча;",[513,530,531,533,534,537,538,541,542,545,546,548],{},[268,532,484],{}," сверяет ",[320,535,536],{},"индекс"," (",[268,539,540],{},"git write-tree","), а не ",[268,543,544],{},"HEAD",", и\nпотому работает и на грязном дереве. Раньше он на ней пропускался — а локально\nдерево грязное почти всегда, так что единственная проверка, ловящая\nрасхождения самого ",[268,547,490],{},", срабатывала только в CI.",[244,550,551,554,555,558,559,561,562,564],{},[320,552,553],{},"Символическая ссылка разводит каналы поставки молча."," ",[268,556,557],{},"cp"," в архив\nразыменовывает её и кладёт содержимое цели, ",[268,560,490],{}," кладёт саму ссылку, а\nобе сверки состава при этом зелёные — они сверяют имена. Поэтому ссылок под\nконтролем git просто не бывает: ",[268,563,354],{}," роняет сборку.",[499,566,568],{"id":567},"про-короткие-теги-отдельно","Про короткие теги отдельно",[244,570,571,573,574,577,578,581,582,584],{},[268,572,403],{}," их ",[320,575,576],{},"не ловит",". При ",[268,579,580],{},"short_open_tag=Off"," — а это значение по\nумолчанию — ",[268,583,417],{}," открывающим тегом не считается, и файл целиком становится\nинлайновым HTML. Синтаксически он остаётся правильным, в нём просто нет PHP.\nА на портале классы из такого файла не определяются, зато исходник уезжает в\nбраузер.",[244,586,587,588,590,591,594,595,597,598,601,602,262],{},"Поэтому ",[268,589,411],{}," спрашивает сам PHP через ",[268,592,593],{},"token_get_all()",", а не\ngrep: ",[268,596,417],{}," внутри строки или комментария лежит в своём токене, опасный же\nостаётся куском ",[268,599,600],{},"T_INLINE_HTML",". Наивный grep краснел бы на каждом регулярном\nвыражении вида ",[268,603,604],{},"\u002F\u003C?\u002F",[264,606,608],{"id":607},"архив","Архив",[244,610,611,612,615,616,618,619,622],{},"Архив содержит ",[320,613,614],{},"каталог модуля целиком",": первым уровнем внутри zip лежит\n",[268,617,497],{},", иначе при распаковке файлы рассыплются прямо по\n",[268,620,621],{},"bitrix\u002Fmodules\u002F",". Проверяется в самом скрипте, а не глазами.",[244,624,625],{},"Сверить поставку двумя путями установки можно так:",[273,627,629],{"className":275,"code":628,"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.insync.zip | grep -v '\u002F$' | sed 's#^shef.insync\u002F##' | sort > \u002Ftmp\u002Fzip.txt\ndiff \u002Ftmp\u002Fcomposer.txt \u002Ftmp\u002Fzip.txt    # должно быть пусто\n",[268,630,631,696,746],{"__ignoreMap":105},[281,632,633,636,640,643,647,649,652,655,659,662,665,668,670,673,676,679,682,685,687,690,693],{"class":283,"line":284},[281,634,635],{"class":287},"git",[281,637,639],{"class":638},"s_sjI"," archive",[281,641,642],{"class":300}," --format=tar",[281,644,646],{"class":645},"sjJ54"," \"$(",[281,648,635],{"class":287},[281,650,651],{"class":638}," write-tree",[281,653,654],{"class":645},")\"",[281,656,658],{"class":657},"smGrS"," |",[281,660,661],{"class":287}," tar",[281,663,664],{"class":300}," -tf",[281,666,667],{"class":638}," -",[281,669,658],{"class":657},[281,671,672],{"class":287}," grep",[281,674,675],{"class":300}," -v",[281,677,678],{"class":645}," '",[281,680,681],{"class":638},"\u002F$",[281,683,684],{"class":645},"'",[281,686,658],{"class":657},[281,688,689],{"class":287}," sort",[281,691,692],{"class":657}," >",[281,694,695],{"class":638}," \u002Ftmp\u002Fcomposer.txt\n",[281,697,698,700,704,707,710,713,715,717,719,721,723,725,727,730,732,735,737,739,741,743],{"class":283,"line":295},[281,699,288],{"class":287},[281,701,703],{"class":702},"sP7_E"," &&",[281,705,706],{"class":287}," unzip",[281,708,709],{"class":300}," -Z1",[281,711,712],{"class":638}," shef.insync.zip",[281,714,658],{"class":657},[281,716,672],{"class":287},[281,718,675],{"class":300},[281,720,678],{"class":645},[281,722,681],{"class":638},[281,724,684],{"class":645},[281,726,658],{"class":657},[281,728,729],{"class":287}," sed",[281,731,678],{"class":645},[281,733,734],{"class":638},"s#^shef.insync\u002F##",[281,736,684],{"class":645},[281,738,658],{"class":657},[281,740,689],{"class":287},[281,742,692],{"class":657},[281,744,745],{"class":638}," \u002Ftmp\u002Fzip.txt\n",[281,747,748,751,754,757],{"class":283,"line":307},[281,749,750],{"class":287},"diff",[281,752,753],{"class":638}," \u002Ftmp\u002Fcomposer.txt",[281,755,756],{"class":638}," \u002Ftmp\u002Fzip.txt",[281,758,759],{"class":291},"    # должно быть пусто\n",[264,761,763],{"id":762},"ci","CI",[244,765,766,769],{},[268,767,768],{},".github\u002Fworkflows\u002Fci.yml",", пять задач:",[329,771,772,782],{},[332,773,774],{},[335,775,776,779],{},[338,777,778],{},"задача",[338,780,781],{},"что делает",[345,783,784,804,817,842,854],{},[335,785,786,795],{},[350,787,788,791,792],{},[268,789,790],{},"PHP 8.2"," … ",[268,793,794],{},"PHP 8.5",[350,796,797,800,801],{},[268,798,799],{},".\u002Fbuild.sh --check",", ",[268,802,803],{},"fail-fast: false",[335,805,806,811],{},[350,807,808],{},[268,809,810],{},"Composer",[350,812,813,816],{},[268,814,815],{},"composer validate --strict",": пакет ставят через Composer, и сломанный манифест виден только тому, кто ставит",[335,818,819,824],{},[350,820,821],{},[268,822,823],{},"Skills",[350,825,826,829,830,833,834,837,838,841],{},[268,827,828],{},"sync.sh --check"," против ",[268,831,832],{},"MANIFEST"," источника в ",[268,835,836],{},"bx-shef\u002Foptions",": навыки линейки здесь — копия, и копия не должна отставать; свои навыки (",[268,839,840],{},"LOCAL.MANIFEST",") источник не сверяет",[335,843,844,849],{},[350,845,846],{},[268,847,848],{},"Build",[350,850,851,853],{},[268,852,288],{}," плюс архив артефактом прогона",[335,855,856,860],{},[350,857,858],{},[268,859,763],{},[350,861,862,863],{},"ворота, ",[268,864,865],{},"needs: [checks, composer, skills, build]",[244,867,868,873,874,877],{},[320,869,870,872],{},[268,871,823],{}," краснеет, когда навыки поправили в shef.options."," Это не поломка\nэтого репозитория, а сигнал: разложите навыки заново\n(",[268,875,876],{},"..\u002Foptions\u002F.claude\u002Fskills\u002Fsync.sh --to .",") и закоммитьте. Копию на месте не\nправят — правка будет затёрта следующей раскладкой.",[244,879,880,554,883,886,887,890],{},[320,881,882],{},"Версии PHP в матрице — не только про код модуля.",[268,884,885],{},"tests\u002Fvendor_test.php","\nзагружает из своей копии библиотек XML ровно то, что загружает модуль, с\n",[268,888,889],{},"error_reporting=-1",": новая версия PHP с новыми deprecation покраснеет здесь, а\nне в логе портала.",[244,892,893,894,896],{},"В защите ветки требуется ровно одна проверка — ",[268,895,763],{},". Остальные её зависимости,\nпоэтому новая задача не потребует правки ruleset.",[499,898,900,901,904],{"id":899},"что-в-ciyml-выглядит-ошибкой-но-ею-не-является","Что в ",[268,902,903],{},"ci.yml"," выглядит ошибкой, но ею не является",[244,906,907,915,916,920,921,924,925,262],{},[320,908,909,912,913],{},[268,910,911],{},"if: always()"," у задачи ",[268,914,763],{}," — обязателен вместе с явной сверкой результатов\nзависимостей. Без него задача была бы ",[917,918,919],"em",{},"пропущена"," при падении зависимости, а\nпропущенную проверку защита ветки засчитывает как ",[917,922,923],{},"пройденную",": красный CI уехал\nбы в ",[268,926,927],{},"main",[244,929,930,931,934],{},"Подмывает заменить на ",[268,932,933],{},"!cancelled()"," — не надо. Тогда отменённый прогон стал бы\nдавать пропущенную проверку, и, отменив прогон вручную, можно было бы смержить\nнепроверенное.",[244,936,937,944],{},[320,938,939,940,943],{},"Вытесненный по ",[268,941,942],{},"concurrency"," прогон краснеет"," на устаревшем коммите. Это шум,\nа не поломка: защита смотрит на проверки головного коммита.",[264,946,948],{"id":947},"релиз","Релиз",[244,950,951,954],{},[268,952,953],{},".github\u002Fworkflows\u002Frelease.yml",", два входа.",[244,956,957,963,964,967,968,970,971,974],{},[320,958,959,960],{},"Пуш тега ",[268,961,962],{},"v*"," — тег ",[320,965,966],{},"сверяется"," с ",[268,969,456],{}," из ",[268,972,973],{},"install\u002Fversion.php",".\nРасхождение роняет прогон: тегу не доверяем, иначе на портал уедет архив,\nверсия которого врёт.",[244,976,977,963,985,970,988,990],{},[320,978,979,982,983],{},[268,980,981],{},"workflow_dispatch"," от ",[268,984,927],{},[320,986,987],{},"выводится",[268,989,456],{}," и ставится\nсам. Запуск от другой ветки отклоняется, занятый тег ловится до сборки.",[244,992,993],{},"Второй вход обязателен: пуш тегов бывает недоступен — другие права, прокси\nсессии, — а релиз выпускать надо.",[244,995,996,999],{},[320,997,998],{},"Тег ставится после успешной сборки."," Поставленный раньше, он пережил бы\nупавшую сборку, и следующая попытка упёрлась бы в занятый тег.",[244,1001,1002,1003,1006,1007,262],{},"Примечания к релизу собираются из секции ",[268,1004,1005],{},"## \u003Cверсия>"," в ",[268,1008,1009],{},"CHANGELOG.md",[499,1011,1013],{"id":1012},"packagist","Packagist",[244,1015,1016,1017,1020,1021,1024,1025,1028,1029,1032],{},"Последним шагом релиз дёргает ",[268,1018,1019],{},"update-package",". Без секретов\n",[268,1022,1023],{},"PACKAGIST_USERNAME"," и ",[268,1026,1027],{},"PACKAGIST_TOKEN"," шаг пропускается, и релиз при этом\n",[320,1030,1031],{},"не падает",": невыложенный релиз чинить нечем, а отставший Packagist\nдогоняется кнопкой Update за десять секунд.",[244,1034,1035,1036,1039,1040,262],{},"Эндпойнт умеет только ",[320,1037,1038],{},"обновлять уже зарегистрированный"," пакет. Первую\nрегистрацию делают один раз руками: packagist.org → Submit →\n",[268,1041,1042],{},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Finsync",[264,1044,1046],{"id":1045},"библиотеки-xml-composer-и-своя-копия","Библиотеки XML: Composer и своя копия",[244,1048,1049,1052,1053,1056,1057,1060,1061,1064,1065,1068],{},[268,1050,1051],{},"composer.json"," требует ",[268,1054,1055],{},"sbwerewolf\u002Fxml-navigator"," — через Composer он и его\nзависимости ложатся в vendor проекта. Архив несёт свою копию в\n",[268,1058,1059],{},"vendor\u002Fsbwerewolf\u002F"," (SHIP). Какую подключать, решает ",[268,1062,1063],{},".settings.php"," при\nкаждой загрузке — по каждому namespace отдельно, через ",[268,1066,1067],{},"ShProjectContext"," из\nshef.options.",[329,1070,1071,1084],{},[332,1072,1073],{},[335,1074,1075,1078,1081],{},[338,1076,1077],{},"пакет",[338,1079,1080],{},"своя копия",[338,1082,1083],{},"namespace",[345,1085,1086,1100,1115],{},[335,1087,1088,1092,1095],{},[350,1089,1090],{},[268,1091,1055],{},[350,1093,1094],{},"7.2.9",[350,1096,1097],{},[268,1098,1099],{},"SbWereWolf\\XmlNavigator",[335,1101,1102,1107,1110],{},[350,1103,1104],{},[268,1105,1106],{},"sbwerewolf\u002Flanguage-specific",[350,1108,1109],{},"8.0.1",[350,1111,1112],{},[268,1113,1114],{},"LanguageSpecific",[335,1116,1117,1122,1125],{},[350,1118,1119],{},[268,1120,1121],{},"sbwerewolf\u002Fjson-serialize-trait",[350,1123,1124],{},"1.0.2",[350,1126,1127],{},[268,1128,1129],{},"SbWereWolf\\JsonSerializable",[244,1131,1132,1133,1136,1137,1139,1140,262],{},"Версий в самих пакетах нет, поэтому они записаны в ",[268,1134,1135],{},"vendor\u002Fversions.json",".\nОбновили копию — обновите и его: ",[268,1138,885],{}," сверяет его с\nограничением в ",[268,1141,1051],{},[244,1143,1144,1147,1148,1151,1152,1155,1156,1159],{},[320,1145,1146],{},"Ветка 7.2 выбрана не случайно."," Ветки 8.x и новее требуют PHP 8.4, а модуль\nподдерживает 8.2. А ",[268,1149,1150],{},"language-specific"," 8.4 переехал в namespace\n",[268,1153,1154],{},"SbWereWolf\\LanguageSpecific",", тогда как xml-navigator 7.2 зовёт\n",[268,1157,1158],{},"LanguageSpecific\\",": своя копия держит связку, которая работает.",[244,1161,1162],{},"Обновить свою копию:",[273,1164,1166],{"className":275,"code":1165,"language":277,"meta":105,"style":105},"mkdir \u002Ftmp\u002Fx && cd \u002Ftmp\u002Fx && echo '{}' > composer.json\ncomposer require sbwerewolf\u002Fxml-navigator:\u003Cверсия> sbwerewolf\u002Flanguage-specific:8.0.*\ncd - && for p in xml-navigator language-specific json-serialize-trait; do\n    rm -rf vendor\u002Fsbwerewolf\u002F$p\u002Fsrc && cp -a \u002Ftmp\u002Fx\u002Fvendor\u002Fsbwerewolf\u002F$p\u002Fsrc vendor\u002Fsbwerewolf\u002F$p\u002F\ndone\n# версии — в vendor\u002Fversions.json\n.\u002Fbuild.sh --check\n",[268,1167,1168,1201,1232,1266,1306,1312,1318],{"__ignoreMap":105},[281,1169,1170,1173,1176,1178,1182,1184,1186,1189,1191,1194,1196,1198],{"class":283,"line":284},[281,1171,1172],{"class":287},"mkdir",[281,1174,1175],{"class":638}," \u002Ftmp\u002Fx",[281,1177,703],{"class":702},[281,1179,1181],{"class":1180},"sptTA"," cd",[281,1183,1175],{"class":638},[281,1185,703],{"class":702},[281,1187,1188],{"class":1180}," echo",[281,1190,678],{"class":645},[281,1192,1193],{"class":638},"{}",[281,1195,684],{"class":645},[281,1197,692],{"class":657},[281,1199,1200],{"class":638}," composer.json\n",[281,1202,1203,1206,1209,1212,1215,1218,1222,1225,1228],{"class":283,"line":295},[281,1204,1205],{"class":287},"composer",[281,1207,1208],{"class":638}," require",[281,1210,1211],{"class":638}," sbwerewolf\u002Fxml-navigator:",[281,1213,1214],{"class":657},"\u003C",[281,1216,1217],{"class":638},"верси",[281,1219,1221],{"class":1220},"su5hD","я",[281,1223,1224],{"class":657},">",[281,1226,1227],{"class":638}," sbwerewolf\u002Flanguage-specific:8.0.",[281,1229,1231],{"class":1230},"s_hVV","*\n",[281,1233,1234,1237,1239,1241,1245,1248,1251,1254,1257,1260,1263],{"class":283,"line":307},[281,1235,1236],{"class":1180},"cd",[281,1238,667],{"class":638},[281,1240,703],{"class":702},[281,1242,1244],{"class":1243},"sVHd0"," for",[281,1246,1247],{"class":1220}," p ",[281,1249,1250],{"class":1243},"in",[281,1252,1253],{"class":638}," xml-navigator",[281,1255,1256],{"class":638}," language-specific",[281,1258,1259],{"class":638}," json-serialize-trait",[281,1261,1262],{"class":702},";",[281,1264,1265],{"class":1243}," do\n",[281,1267,1269,1272,1275,1278,1281,1284,1286,1289,1292,1295,1297,1299,1301,1303],{"class":283,"line":1268},4,[281,1270,1271],{"class":287},"    rm",[281,1273,1274],{"class":300}," -rf",[281,1276,1277],{"class":638}," vendor\u002Fsbwerewolf\u002F",[281,1279,1280],{"class":1220},"$p",[281,1282,1283],{"class":638},"\u002Fsrc",[281,1285,703],{"class":702},[281,1287,1288],{"class":287}," cp",[281,1290,1291],{"class":300}," -a",[281,1293,1294],{"class":638}," \u002Ftmp\u002Fx\u002Fvendor\u002Fsbwerewolf\u002F",[281,1296,1280],{"class":1220},[281,1298,1283],{"class":638},[281,1300,1277],{"class":638},[281,1302,1280],{"class":1220},[281,1304,1305],{"class":638},"\u002F\n",[281,1307,1309],{"class":283,"line":1308},5,[281,1310,1311],{"class":1243},"done\n",[281,1313,1315],{"class":283,"line":1314},6,[281,1316,1317],{"class":291},"# версии — в vendor\u002Fversions.json\n",[281,1319,1321,1323],{"class":283,"line":1320},7,[281,1322,288],{"class":287},[281,1324,1325],{"class":300}," --check\n",[264,1327,1329],{"id":1328},"куда-composer-кладёт-модуль","Куда Composer кладёт модуль",[244,1331,1332,1334,1335,1338,1339,1342,1343,1346,1347,1350,1351,1354,1355,1358,1359,1361],{},[268,1333,1051],{},": ",[268,1336,1337],{},"type"," = ",[268,1340,1341],{},"bitrix-module"," плюс\n",[268,1344,1345],{},"extra.installer-name = shef.insync",". Тогда Composer разворачивает модуль в\n",[268,1348,1349],{},"bitrix\u002Fmodules\u002Fshef.insync\u002F"," без настройки на стороне потребителя:\n",[268,1352,1353],{},"installer-name"," читается из пакета, а ",[268,1356,1357],{},"{$bitrix_dir}"," — только из корневого\n",[268,1360,1051],{},", повлиять на него пакет не может.",[244,1363,1364,1370,1371,1374],{},[320,1365,1366,1369],{},[268,1367,1368],{},"bitrix-d7-module"," развернул бы модуль не туда."," Шаблоны в\n",[268,1372,1373],{},"composer\u002Finstallers",":",[510,1376,1377,1385],{},[513,1378,1379,1381,1382],{},[268,1380,1341],{}," → ",[268,1383,1384],{},"{$bitrix_dir}\u002Fmodules\u002F{$name}\u002F",[513,1386,1387,1381,1389],{},[268,1388,1368],{},[268,1390,1391],{},"{$bitrix_dir}\u002Fmodules\u002F{$vendor}.{$name}\u002F",[244,1393,1394,1395,1397,1398,1401,1402,1405,1406,1409],{},"а ",[268,1396,1353],{}," подменяет только ",[268,1399,1400],{},"{$name}",". Для пакета ",[268,1403,1404],{},"bxshef\u002Finsync","\nвторой вариант дал бы ",[268,1407,1408],{},"bitrix\u002Fmodules\u002Fbxshef.shef.insync\u002F"," — каталог, которого\nБитрикс не знает.",[244,1411,1412,1413,1374],{},"На стороне проекта-потребителя Composer 2.2+ требует явного разрешения\nплагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в\n",[268,1414,1415],{},"vendor\u002Fbxshef\u002Finsync",[273,1417,1421],{"className":1418,"code":1419,"language":1420,"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,1422,1423,1428,1446,1461,1476,1481,1486],{"__ignoreMap":105},[281,1424,1425],{"class":283,"line":284},[281,1426,1427],{"class":702},"{\n",[281,1429,1430,1434,1438,1441,1443],{"class":283,"line":295},[281,1431,1433],{"class":1432},"s39Yj","    \"",[281,1435,1437],{"class":1436},"sseR_","config",[281,1439,1440],{"class":1432},"\"",[281,1442,1374],{"class":702},[281,1444,1445],{"class":702}," {\n",[281,1447,1448,1451,1455,1457,1459],{"class":283,"line":307},[281,1449,1450],{"class":1432},"        \"",[281,1452,1454],{"class":1453},"sZMiF","allow-plugins",[281,1456,1440],{"class":1432},[281,1458,1374],{"class":702},[281,1460,1445],{"class":702},[281,1462,1463,1466,1469,1471,1473],{"class":283,"line":1268},[281,1464,1465],{"class":1432},"            \"",[281,1467,1373],{"class":1468},"srdBf",[281,1470,1440],{"class":1432},[281,1472,1374],{"class":702},[281,1474,1475],{"class":1432}," true\n",[281,1477,1478],{"class":283,"line":1308},[281,1479,1480],{"class":702},"        }\n",[281,1482,1483],{"class":283,"line":1314},[281,1484,1485],{"class":702},"    }\n",[281,1487,1488],{"class":283,"line":1320},[281,1489,1490],{"class":702},"}\n",[244,1492,1493,1495,1496,1498,1499,1502,1503,1506,1507,1510],{},[268,1494,1341],{}," помечен в исходниках ",[268,1497,1373],{}," как ",[268,1500,1501],{},"deprecated, remove on the major release",", поэтому в ",[268,1504,1505],{},"require"," стоит потолок\n",[268,1508,1509],{},"\"composer\u002Finstallers\": \"^1.0 || ^2.0\"",". Снимут потолок — модуль уедет в чужой\nкаталог.",[264,1512,92],{"id":1513},"проверка-на-портале",[244,1515,1516,1517,1520,1521,1524,1525,262],{},"Каталог модуля браузеру недоступен: в поставке nginx стоит ",[268,1518,1519],{},"deny all"," на\n",[268,1522,1523],{},"^\u002Fbitrix\u002F(modules|local_cache|stack_cache|managed_cache|php_interface)",".\nПоэтому фронт и раскладывается в ",[268,1526,1527],{},"\u002Fbitrix\u002Fjs",[244,1529,1530],{},"Проверить на стенде:",[273,1532,1537],{"className":1533,"code":1535,"language":1536},[1534],"language-text","\u002Fbitrix\u002Fmodules\u002Fshef.insync\u002Finstall\u002Fjs\u002Fshef-insync\u002Fui-anchors\u002Fscript.js  -> 403\n\u002Fbitrix\u002Fjs\u002Fshef-insync\u002Fui-anchors\u002Fscript.js                              -> 200\n\u002Fbitrix\u002Fcomponents\u002Fshef.insync\u002Fimport.stat.local\u002Fclass.php                -> есть на диске\n\u002Flocal\u002Fcomponents\u002Fshef.insync\u002F                                            -> нет (копия 1.x убрана)\n","text",[268,1538,1535],{"__ignoreMap":105},[244,1540,1541,1542,262],{},"Каталоги импорта — вне корня сайта, ссылки на них нет вовсе, см. ",[248,1543,1544],{"href":200},"security.md",[244,1546,1547,1548,1551],{},"Полная процедура проверки на портале — в ",[248,1549,1550],{"href":195},"portal-check.md",":\nшаги с ожидаемым результатом, отдельно обновление с 1.x и запуск\nпримеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта\nпроцедура и есть тест.",[1553,1554,1555],"note",{},[244,1556,1557,1558,1563],{},"Источник: ",[248,1559,1562],{"href":1560,"rel":1561},"https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Finsync\u002Fblob\u002Fmain\u002Fdocs\u002Fbuild-and-install.md",[256],"insync\u002Fdocs\u002Fbuild-and-install.md"," — правки туда, сайт пересобирается сам.",[1565,1566,1567],"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 .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 .sptTA, html code.shiki .sptTA{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}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}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":105,"searchDepth":307,"depth":307,"links":1569},[1570,1572,1576,1577,1581,1584,1585,1586],{"id":266,"depth":295,"text":1571},"build.sh — единственная точка входа",{"id":326,"depth":295,"text":327,"children":1573},[1574,1575],{"id":501,"depth":307,"text":502},{"id":567,"depth":307,"text":568},{"id":607,"depth":295,"text":608},{"id":762,"depth":295,"text":763,"children":1578},[1579],{"id":899,"depth":307,"text":1580},"Что в ci.yml выглядит ошибкой, но ею не является",{"id":947,"depth":295,"text":948,"children":1582},[1583],{"id":1012,"depth":307,"text":1013},{"id":1045,"depth":295,"text":1046},{"id":1328,"depth":295,"text":1329},{"id":1513,"depth":295,"text":92},"md",{},true,"---\ntitle: \"Сборка, CI и релиз\"\n---\n\nРаскладка репозитория — в [module-structure.md](\u002Fmodules\u002Finsync\u002Fmodule-structure), процесс —\nв [CONTRIBUTING.md](https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Finsync\u002Fblob\u002Fmain\u002FCONTRIBUTING.md), установка глазами пользователя —\nв [README.md](\u002Fmodules\u002Finsync).\n\n## `build.sh` — единственная точка входа\n\n```bash\n.\u002Fbuild.sh            # проверки + архив shef.insync.zip\n.\u002Fbuild.sh --check    # только проверки\n.\u002Fbuild.sh --version  # напечатать версию модуля\n```\n\nCI зовёт **её же**. Это не украшение: если бы сервер гонял свой набор команд,\nлокальный зелёный прогон и серверный красный означали бы разные вещи, и\nразбираться пришлось бы в двух местах сразу.\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_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.insync\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.insync\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.insync.zip | grep -v '\u002F$' | sed 's#^shef.insync\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| `Composer` | `composer validate --strict`: пакет ставят через Composer, и сломанный манифест виден только тому, кто ставит |\n| `Skills` | `sync.sh --check` против `MANIFEST` источника в `bx-shef\u002Foptions`: навыки линейки здесь — копия, и копия не должна отставать; свои навыки (`LOCAL.MANIFEST`) источник не сверяет |\n| `Build` | `.\u002Fbuild.sh` плюс архив артефактом прогона |\n| `CI` | ворота, `needs: [checks, composer, skills, build]` |\n\n**`Skills` краснеет, когда навыки поправили в shef.options.** Это не поломка\nэтого репозитория, а сигнал: разложите навыки заново\n(`..\u002Foptions\u002F.claude\u002Fskills\u002Fsync.sh --to .`) и закоммитьте. Копию на месте не\nправят — правка будет затёрта следующей раскладкой.\n\n**Версии PHP в матрице — не только про код модуля.** `tests\u002Fvendor_test.php`\nзагружает из своей копии библиотек XML ровно то, что загружает модуль, с\n`error_reporting=-1`: новая версия PHP с новыми deprecation покраснеет здесь, а\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Примечания к релизу собираются из секции `## \u003Cверсия>` в `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\u002Finsync`.\n\n## Библиотеки XML: Composer и своя копия\n\n`composer.json` требует `sbwerewolf\u002Fxml-navigator` — через Composer он и его\nзависимости ложатся в vendor проекта. Архив несёт свою копию в\n`vendor\u002Fsbwerewolf\u002F` (SHIP). Какую подключать, решает `.settings.php` при\nкаждой загрузке — по каждому namespace отдельно, через `ShProjectContext` из\nshef.options.\n\n| пакет | своя копия | namespace |\n|---|---|---|\n| `sbwerewolf\u002Fxml-navigator` | 7.2.9 | `SbWereWolf\\XmlNavigator` |\n| `sbwerewolf\u002Flanguage-specific` | 8.0.1 | `LanguageSpecific` |\n| `sbwerewolf\u002Fjson-serialize-trait` | 1.0.2 | `SbWereWolf\\JsonSerializable` |\n\nВерсий в самих пакетах нет, поэтому они записаны в `vendor\u002Fversions.json`.\nОбновили копию — обновите и его: `tests\u002Fvendor_test.php` сверяет его с\nограничением в `composer.json`.\n\n**Ветка 7.2 выбрана не случайно.** Ветки 8.x и новее требуют PHP 8.4, а модуль\nподдерживает 8.2. А `language-specific` 8.4 переехал в namespace\n`SbWereWolf\\LanguageSpecific`, тогда как xml-navigator 7.2 зовёт\n`LanguageSpecific\\`: своя копия держит связку, которая работает.\n\nОбновить свою копию:\n\n```bash\nmkdir \u002Ftmp\u002Fx && cd \u002Ftmp\u002Fx && echo '{}' > composer.json\ncomposer require sbwerewolf\u002Fxml-navigator:\u003Cверсия> sbwerewolf\u002Flanguage-specific:8.0.*\ncd - && for p in xml-navigator language-specific json-serialize-trait; do\n\trm -rf vendor\u002Fsbwerewolf\u002F$p\u002Fsrc && cp -a \u002Ftmp\u002Fx\u002Fvendor\u002Fsbwerewolf\u002F$p\u002Fsrc vendor\u002Fsbwerewolf\u002F$p\u002F\ndone\n# версии — в vendor\u002Fversions.json\n.\u002Fbuild.sh --check\n```\n\n## Куда Composer кладёт модуль\n\n`composer.json`: `type` = `bitrix-module` плюс\n`extra.installer-name = shef.insync`. Тогда Composer разворачивает модуль в\n`bitrix\u002Fmodules\u002Fshef.insync\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\u002Finsync`\nвторой вариант дал бы `bitrix\u002Fmodules\u002Fbxshef.shef.insync\u002F` — каталог, которого\nБитрикс не знает.\n\nНа стороне проекта-потребителя Composer 2.2+ требует явного разрешения\nплагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в\n`vendor\u002Fbxshef\u002Finsync`:\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\u002Fjs`.\n\nПроверить на стенде:\n\n```\n\u002Fbitrix\u002Fmodules\u002Fshef.insync\u002Finstall\u002Fjs\u002Fshef-insync\u002Fui-anchors\u002Fscript.js  -> 403\n\u002Fbitrix\u002Fjs\u002Fshef-insync\u002Fui-anchors\u002Fscript.js                              -> 200\n\u002Fbitrix\u002Fcomponents\u002Fshef.insync\u002Fimport.stat.local\u002Fclass.php                -> есть на диске\n\u002Flocal\u002Fcomponents\u002Fshef.insync\u002F                                            -> нет (копия 1.x убрана)\n```\n\nКаталоги импорта — вне корня сайта, ссылки на них нет вовсе, см. [security.md](\u002Fmodules\u002Finsync\u002Fsecurity).\n\nПолная процедура проверки на портале — в [portal-check.md](\u002Fmodules\u002Finsync\u002Fportal-check):\nшаги с ожидаемым результатом, отдельно обновление с 1.x и запуск\nпримеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта\nпроцедура и есть тест.\n\n::note\nИсточник: [insync\u002Fdocs\u002Fbuild-and-install.md](https:\u002F\u002Fgithub.com\u002Fbx-shef\u002Finsync\u002Fblob\u002Fmain\u002Fdocs\u002Fbuild-and-install.md) — правки туда, сайт пересобирается сам.\n::\n\n",{"title":82,"description":85},"zZHwml80pEcM3JVfjHVvuyz27wZ5CKag4H0oXajWnY0",[1594,6],{"title":128,"path":233,"stem":234,"description":105,"children":-1},1790857270365]