Раскладка репозитория — в module-structure.md, процесс — в CONTRIBUTING.md, установка глазами пользователя — в README.md.
В статье:
Раскладка репозитория — в module-structure.md, процесс — в CONTRIBUTING.md, установка глазами пользователя — в README.md.
build.sh — единственная точка входа сборки./build.sh # проверки + архив shef.options.zip
./build.sh --check # только проверки
./build.sh --version # напечатать версию модуля
./build.sh --notes # примечания к релизу из CHANGELOG
./build.sh --notes 3.0.6 # то же, но отсчёт от указанного выпуска
CI зовёт её же. Это не украшение: если бы сервер гонял свой набор команд, локальный зелёный прогон и серверный красный означали бы разные вещи, и разбираться пришлось бы в двух местах сразу.
composer install # поднять инструменты разработчика
composer run lint # сухой прогон: покажет диф и упадёт
composer run lint:fix # привести файлы
php-cs-fixer, правила в .php-cs-fixer.dist.php, набор — @PSR12 целиком.
Из build.sh он НЕ зовётся, и это решение, а не недоделка: сборке хватает
php, git и zip, и она обязана отрабатывать в свежем клоне. Позови она
линтер — ./build.sh --check перестал бы запускаться, пока не сделан
composer install, то есть проверка поставки начала бы зависеть от сети.
Поэтому проверки две, и обе обязательны в CI.
Версия инструмента в composer.json пришпилена точно, без ^: набор
@PSR12 у php-cs-fixer пополняется в минорных выпусках, и с ^3.0 CI однажды
покраснел бы на коммите, который ничего не менял. Обновление — осознанная
правка одной строки, следом composer update и новый composer.lock.
composer.lock под контролем git — в отличие от обычая для библиотек, и
намеренно. Точная версия пришпиливает сам php-cs-fixer, но не три десятка его
зависимостей: без lock CI разрешал бы их заново на каждом прогоне. На
зависимости пакета это не влияет — Composer читает lock только корневого
проекта, а в поставку файл не едет (KEEP плюс export-ignore).
config.platform.php = 8.2.0 — тоже не украшение. Без него Composer
разрешает зависимости под тот PHP, на котором запущен, и lock, собранный на
8.4, на нижней границе поддержки не ставится: измерено на CI —
symfony/string v8.1.7 и sebastian/diff 9.0.1 требуют 8.4. С пином lock
годится для всех версий из матрицы. На потребителей пакета это не влияет:
config Composer читает только у корневого проекта.
config.allow-plugins в composer.json — не украшение: composer/installers
это плагин, а Composer с 2.2 по умолчанию блокирует плагины и в неинтерактивном
режиме просто падает. Без этого ключа задача Lint не доходила даже до
линтера.
Чего линтер не ловит: отступы в инлайновом HTML. Строки между ?> и <?php
для него текст, а не код. См. CLAUDE.md, «Линтер не видит отступы в инлайновом
HTML».
При сборке дополнительно: состав zip сверяется со списком SHIP, а первый
уровень внутри архива — с shef.options/.
Пропущенная проверка выглядит как пройденная. Поэтому:
node, а JS или *_test.mjs в репозитории есть — отказ, а не примечание;run_tests сначала считает, сколько тестов ЕСТЬ, и сверяет с числом
прогнанных: переименованный файл или тест в подкаталоге иначе выпал бы из
прогона молча;check_composer_package сверяет индекс (git write-tree), а не HEAD, и
потому работает и на грязном дереве. Раньше он на ней пропускался — а локально
дерево грязное почти всегда, так что единственная проверка, ловящая
расхождения самого git archive, срабатывала только в CI.Символическая ссылка разводит каналы поставки молча. cp в архив
разыменовывает её и кладёт содержимое цели, git archive кладёт саму ссылку, а
обе сверки состава при этом зелёные — они сверяют имена. Поэтому ссылок под
контролем git просто не бывает: check_filenames роняет сборку.
php -l их не ловит. При short_open_tag=Off — а это значение по
умолчанию — <? открывающим тегом не считается, и файл целиком становится
инлайновым HTML. Синтаксически он остаётся правильным, в нём просто нет PHP.
А на портале классы из такого файла не определяются, зато исходник уезжает в
браузер.
Поэтому check_short_tags спрашивает сам PHP через token_get_all(), а не
grep: <? внутри строки или комментария лежит в своём токене, опасный же
остаётся куском T_INLINE_HTML. Наивный grep краснел бы на каждом регулярном
выражении вида /<?/.
Архив содержит каталог модуля целиком: первым уровнем внутри zip лежит
shef.options/, иначе при распаковке файлы рассыплются прямо по
bitrix/modules/. Проверяется в самом скрипте, а не глазами.
Сверить поставку двумя путями установки можно так:
git archive --format=tar "$(git write-tree)" | tar -tf - | grep -v '/$' | sort > /tmp/composer.txt
./build.sh && unzip -Z1 shef.options.zip | grep -v '/$' | sed 's#^shef.options/##' | sort > /tmp/zip.txt
diff /tmp/composer.txt /tmp/zip.txt # должно быть пусто
.github/workflows/ci.yml, четыре задачи:
Lint гоняется на одной версии PHP, а не на матрице: форматирование от версии
рантайма не зависит, а четыре одинаковых прогона только тянули бы время.
В защите ветки требуется ровно одна проверка — CI. Остальные её зависимости,
поэтому новая задача не потребует правки ruleset.
ci.yml выглядит ошибкой, но ею не являетсяif: always() у задачи CI — обязателен вместе с явной сверкой результатов
зависимостей. Без него задача была бы пропущена при падении зависимости, а
пропущенную проверку защита ветки засчитывает как пройденную: красный CI уехал
бы в main.
Подмывает заменить на !cancelled() — не надо. Тогда отменённый прогон стал бы
давать пропущенную проверку, и, отменив прогон вручную, можно было бы смержить
непроверенное.
Вытесненный по concurrency прогон краснеет на устаревшем коммите. Это шум,
а не поломка: защита смотрит на проверки головного коммита.
.github/workflows/release.yml, два входа.
Пуш тега v* — тег сверяется с VERSION из install/version.php.
Расхождение роняет прогон: тегу не доверяем, иначе на портал уедет архив,
версия которого врёт.
workflow_dispatch от main — тег выводится из VERSION и ставится
сам. Запуск от другой ветки отклоняется, занятый тег ловится до сборки.
Второй вход обязателен: пуш тегов бывает недоступен — другие права, прокси сессии, — а релиз выпускать надо.
Тег ставится после успешной сборки. Поставленный раньше, он пережил бы упавшую сборку, и следующая попытка упёрлась бы в занятый тег.
Примечания к релизу собирает ./build.sh --notes — секции CHANGELOG.md
от текущей версии до предыдущего выпущенного тега, не включая его.
Раньше бралась одна секция текущей версии, и это молча теряло всё, что слили
в main, но не выпустили. Между v3.0.6 и v3.0.14 так накопилось семь
секций: на странице релиза стоял один линтер, а в архиве лежали ещё и починка
toArray(), \Stringable и снятие _log1().
Заголовок текущей версии не печатается — он и так стоит заголовком релиза. Заголовки версий, которые отдельным релизом не выходили, наоборот нужны: без них бульеты нескольких выпусков слиплись бы в один список. Перед ними встаёт строка «Версии ниже отдельными релизами не выпускались — их изменения в этом архиве».
Предыдущий выпуск ищется по тегам: самый старший vX.Y.Z, который строго
младше текущей версии и достижим из HEAD. Оба условия про одно и то
же — не начать отсчёт от версии, секции которой в этом CHANGELOG.md нет.
Тег выше текущей версии так и выглядит; тег на ветке поддержки или
поставленный руками мимо main — тоже, только снизу. Сравниваются числа по
трём частям, а не строки: строкой «3.0.9» больше «3.0.14», а git и вовсе
печатает v3.0.14 раньше v3.0.2.
Три случая, и все три ведут себя одинаково: примечания идут до конца файла, а строка «Версии ниже отдельными релизами не выпускались» не печатается — под ней оказались бы выпущенные версии, и страница релиза утверждала бы неправду ровно тогда, когда что-то пошло не так.
Третий случай — это и ./build.sh --notes <текущая версия>: спутать легко, а
молча уехала бы вся история модуля.
Обратного случая — секции текущей версии нет, примечания пустые — ждать
не надо: его ловит check_changelog_section в ./build.sh --check, то есть
в PR, а не в момент выпуска.
Логика лежит в build.sh, а не в теле workflow, по одной причине: то же
самое получается локально одной командой, и на неё написан тест
(tests/release_notes_test.php). Код в yaml не проверяется ничем, кроме
выпуска релиза.
Выбранный предыдущий выпуск печатается в stderr — в журнале релиза видно, от
чего шёл отсчёт. Локально перед прогоном нужны свежие теги: в клоне, где их
нет совсем, в примечания уйдёт весь CHANGELOG.md, а где они протухли —
отсчёт пойдёт от последнего известного, и примечаний окажется больше, чем
надо.
git fetch --tags origin && ./build.sh --notes
Собрать примечания к уже выпущенному релизу — тем же режимом, аргументом. Так восстанавливается текст для выпуска, который вышел с неполными примечаниями:
mkdir -p /tmp/notes/install && cp build.sh /tmp/notes/
git show v3.0.14:CHANGELOG.md > /tmp/notes/CHANGELOG.md
git show v3.0.14:install/version.php > /tmp/notes/install/version.php
/tmp/notes/build.sh --notes 3.0.6
Пустые примечания — не ошибка: workflow подставит «Версия X. Изменения — в
CHANGELOG.md». Ненулевой код возврата уронил бы выпуск из-за оформления
CHANGELOG.md.
Последним шагом релиз дёргает update-package. Без секретов
PACKAGIST_USERNAME и PACKAGIST_TOKEN шаг пропускается, и релиз при этом
не падает: невыложенный релиз чинить нечем, а отставший Packagist
догоняется кнопкой Update за десять секунд.
Эндпойнт умеет только обновлять уже зарегистрированный пакет. Первую
регистрацию делают один раз руками: packagist.org → Submit →
https://github.com/bx-shef/options.
composer.json: type = bitrix-module плюс
extra.installer-name = shef.options. Тогда Composer разворачивает модуль в
bitrix/modules/shef.options/ без настройки на стороне потребителя:
installer-name читается из пакета, а {$bitrix_dir} — только из корневого
composer.json, повлиять на него пакет не может.
bitrix-d7-module развернул бы модуль не туда. Шаблоны в
composer/installers:
bitrix-module → {$bitrix_dir}/modules/{$name}/bitrix-d7-module → {$bitrix_dir}/modules/{$vendor}.{$name}/а installer-name подменяет только {$name}. Для пакета bxshef/options
второй вариант дал бы bitrix/modules/bxshef.shef.options/ — каталог, которого
Битрикс не знает.
На стороне проекта-потребителя Composer 2.2+ требует явного разрешения
плагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в
vendor/bxshef/options:
{
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
bitrix-module помечен в исходниках composer/installers как deprecated, remove on the major release, поэтому в require стоит потолок
"composer/installers": "^1.0 || ^2.0". Снимут потолок — модуль уедет в чужой
каталог.
Каталог модуля браузеру недоступен: в поставке nginx стоит deny all на
^/bitrix/(modules|local_cache|stack_cache|managed_cache|php_interface).
Поэтому фронт и раскладывается в /bitrix/css и /bitrix/js.
Проверить на стенде:
/bitrix/modules/shef.options/js/... -> 403
/bitrix/css/shef.options/admin-options.css -> 200
Полная процедура проверки на портале — в portal-check.md: десять шагов с ожидаемым результатом, отдельно обновление с 2.x и запуск примеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта процедура и есть тест.