Сборка, CI и релиз

Раскладка репозитория — в module-structure.md, процесс — в CONTRIBUTING.md, установка глазами пользователя — в README.md.

Раскладка репозитория — в module-structure.md, процесс — в CONTRIBUTING.md, установка глазами пользователя — в README.md.

build.sh — точка входа сборки и проверок

Вторая проверка — линтер, отдельной целью Composer: см. раздел ниже.

./build.sh            # проверки + архив shef.problems.zip
./build.sh --check    # только проверки
./build.sh --version  # напечатать версию модуля

CI зовёт её же. Это не украшение: если бы сервер гонял свой набор команд, локальный зелёный прогон и серверный красный означали бы разные вещи, и разбираться пришлось бы в двух местах сразу.

Линтер — рядом со сборкой, а не внутри

composer install         # один раз: инструменты разработчика в vendor-dev/
composer run lint        # сухой прогон: покажет диф и упадёт
composer run lint:fix    # привести файлы

php-cs-fixer, набор @PSR12 целиком, правила — .php-cs-fixer.dist.php. Версия инструмента пришпилена точно (3.95.27, без ^): набор @PSR12 пополняется в минорных выпусках, и с ^3.0 CI однажды покраснел бы на коммите, который ничего не менял. Его зависимости держит composer.lock — он под git, вопреки обычаю для библиотек; config.platform.php = 8.2.0, иначе lock разрешился бы под PHP того, кто его собирал, и на 8.2 не встал бы.

Из build.sh линтер не зовётся: сборке хватает php, git и zip, и она обязана отрабатывать в свежем клоне. Позови она линтер — ./build.sh --check зависел бы от сети.

vendor-dev/, а не vendor/. vendor/ здесь — своя копия Monolog под git, она едет в поставку. Поставь Composer инструменты туда, composer install переписал бы копию той версией Monolog, что разрешилась в lock. Плагин composer/installers в корневом composer.json выключен (false): иначе он разложил бы bxshef/options в bitrix/modules/ посреди репозитория. Потребителей пакета это не касается — config Composer читает только у корневого проекта.

Линтер правит PHP-токены. Инлайновый HTML между ?> и <?php он не трогает — его раскладка доводится руками; содержимое строковых литералов не трогает ни он, ни рука: это данные программы.

Что проверяется

проверкачто ловит
check_filenamesимя, с которым не справится скрипт; символическую ссылку под контролем git
check_listsфайл, не попавший ни в SHIP, ни в KEEP
check_gitattributesрасхождение KEEP и export-ignore — в обе стороны
check_encodingфайл не в UTF-8, BOM в начале файла
check_phpphp -l по всем PHP
check_short_tagsкороткий тег <?
check_jsnode --check по всем JS
check_lowercaseзаглавные буквы в путях lib/
check_versionпустой или кривой VERSION, пустой VERSION_DATE
run_teststests/*_test.php (php) и tests/*_test.mjs (node)
check_composer_packageсостав git archive разошёлся со списком SHIP

При сборке дополнительно: состав zip сверяется со списком SHIP, а первый уровень внутри архива — с shef.problems/.

Что здесь сделано «строже, чем хотелось»

Пропущенная проверка выглядит как пройденная. Поэтому:

  • нет 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.problems/, иначе при распаковке файлы рассыплются прямо по bitrix/modules/. Проверяется в самом скрипте, а не глазами.

Сверить поставку двумя путями установки можно так:

git archive --format=tar "$(git write-tree)" | tar -tf - | grep -v '/$' | sort > /tmp/composer.txt
./build.sh && unzip -Z1 shef.problems.zip | grep -v '/$' | sed 's#^shef.problems/##' | sort > /tmp/zip.txt
diff /tmp/composer.txt /tmp/zip.txt    # должно быть пусто

CI

.github/workflows/ci.yml, шесть задач:

задачачто делает
PHP 8.2 … PHP 8.5./build.sh --check, fail-fast: false
Composercomposer validate --strict: пакет ставят через Composer, и сломанный манифест виден только тому, кто ставит; заодно свежесть composer.lock
Skillssync.sh --check против MANIFEST источника в bx-shef/options: навыки здесь — копия, и копия не должна отставать
Build./build.sh плюс архив артефактом прогона
Lintcomposer install и composer run lint, одна версия PHP — 8.2
CIворота, needs: [checks, composer, skills, build, lint]

Skills краснеет, когда навыки поправили в shef.options. Это не поломка этого репозитория, а сигнал: разложите навыки заново (../options/.claude/skills/sync.sh --to .) и закоммитьте. Копию на месте не правят — правка будет затёрта следующей раскладкой.

Версии PHP в матрице — не только про код модуля. tests/vendor_test.php разбирает все файлы своей копии Monolog с error_reporting=-1: новая версия PHP с новыми deprecation покраснеет здесь, а не в логе портала. Так уже было: Monolog 3.3.1 на PHP 8.4 сыпал deprecation из Monolog\Logger.

В защите ветки требуется ровно одна проверка — CI. Остальные её зависимости, поэтому новая задача не потребует правки ruleset.

Что в ci.yml выглядит ошибкой, но ею не является

if: always() у задачи CI — обязателен вместе с явной сверкой результатов зависимостей. Без него задача была бы пропущена при падении зависимости, а пропущенную проверку защита ветки засчитывает как пройденную: красный CI уехал бы в main.

Подмывает заменить на !cancelled() — не надо. Тогда отменённый прогон стал бы давать пропущенную проверку, и, отменив прогон вручную, можно было бы смержить непроверенное.

Вытесненный по concurrency прогон краснеет на устаревшем коммите. Это шум, а не поломка: защита смотрит на проверки головного коммита.

Релиз

.github/workflows/release.yml, два входа.

Пуш тега v* — тег сверяется с VERSION из install/version.php. Расхождение роняет прогон: тегу не доверяем, иначе на портал уедет архив, версия которого врёт.

workflow_dispatch от main — тег выводится из VERSION и ставится сам. Запуск от другой ветки отклоняется, занятый тег ловится до сборки.

Второй вход обязателен: пуш тегов бывает недоступен — другие права, прокси сессии, — а релиз выпускать надо.

Тег ставится после успешной сборки. Поставленный раньше, он пережил бы упавшую сборку, и следующая попытка упёрлась бы в занятый тег.

Примечания к релизу собираются из секции ## <версия> в CHANGELOG.md.

Packagist

Последним шагом релиз дёргает update-package. Без секретов PACKAGIST_USERNAME и PACKAGIST_TOKEN шаг пропускается, и релиз при этом не падает: невыложенный релиз чинить нечем, а отставший Packagist догоняется кнопкой Update за десять секунд.

Эндпойнт умеет только обновлять уже зарегистрированный пакет. Первую регистрацию делают один раз руками: packagist.org → Submit → https://github.com/bx-shef/problems.

Monolog: Composer и своя копия

composer.json требует monolog/monolog — через Composer он ложится в vendor проекта. Архив несёт свою копию в vendor/monolog/monolog (SHIP). Какую подключать, решает .settings.php при каждой загрузке, см. Monolog.

Обновить свою копию:

git clone --depth 1 --branch <версия> https://github.com/Seldaek/monolog.git /tmp/monolog
rm -rf vendor/monolog/monolog && mkdir -p vendor/monolog/monolog
cp -a /tmp/monolog/{src,LICENSE,README.md,CHANGELOG.md,composer.json} vendor/monolog/monolog/
./build.sh --check

Версия копии обязана подходить под ограничение в composer.json — иначе поставленный архивом и поставленный Composer модуль работали бы на разном Monolog. Сторожит tests/vendor_test.php, версию он читает из первой записи CHANGELOG.md копии.

Куда Composer кладёт модуль

composer.json: type = bitrix-module плюс extra.installer-name = shef.problems. Тогда Composer разворачивает модуль в bitrix/modules/shef.problems/ без настройки на стороне потребителя: 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/problems второй вариант дал бы bitrix/modules/bxshef.shef.problems/ — каталог, которого Битрикс не знает.

На стороне проекта-потребителя Composer 2.2+ требует явного разрешения плагина, иначе в неинтерактивном режиме (CI) он не отработает и пакет ляжет в vendor/bxshef/problems:

{
    "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/js.

Проверить на стенде:

/bitrix/modules/shef.problems/install/js/shef-problems/monolog-pr-html/style.css  -> 403
/bitrix/js/shef-problems/monolog-pr-html/style.css                              -> 200
/bitrix/admin/shef_problems_logs.php                                            -> страница логов, только администратору

Логи лежат вне корня сайта, ссылки на них нет вовсе — см. security.md.

Полная процедура проверки на портале — в portal-check.md: шаги с ожидаемым результатом, отдельно обновление с 1.x и запуск примеров на живом ядре. Тестами рантайм Битрикса не покрыть, поэтому эта процедура и есть тест.

Источник: problems/docs/build-and-install.md — правки туда, сайт пересобирается сам.
CtrlI