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

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

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

build.sh — единственная точка входа

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

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

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

проверкачто ловит
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.insync/.

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

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

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

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

git archive --format=tar "$(git write-tree)" | tar -tf - | grep -v '/$' | sort > /tmp/composer.txt
./build.sh && unzip -Z1 shef.insync.zip | grep -v '/$' | sed 's#^shef.insync/##' | 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, и сломанный манифест виден только тому, кто ставит
Skillssync.sh --check против MANIFEST источника в bx-shef/options: навыки линейки здесь — копия, и копия не должна отставать; свои навыки (LOCAL.MANIFEST) источник не сверяет
Build./build.sh плюс архив артефактом прогона
CIворота, needs: [checks, composer, skills, build]

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

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

В защите ветки требуется ровно одна проверка — 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/insync.

Библиотеки XML: Composer и своя копия

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

пакетсвоя копияnamespace
sbwerewolf/xml-navigator7.2.9SbWereWolf\XmlNavigator
sbwerewolf/language-specific8.0.1LanguageSpecific
sbwerewolf/json-serialize-trait1.0.2SbWereWolf\JsonSerializable

Версий в самих пакетах нет, поэтому они записаны в vendor/versions.json. Обновили копию — обновите и его: tests/vendor_test.php сверяет его с ограничением в composer.json.

Ветка 7.2 выбрана не случайно. Ветки 8.x и новее требуют PHP 8.4, а модуль поддерживает 8.2. А language-specific 8.4 переехал в namespace SbWereWolf\LanguageSpecific, тогда как xml-navigator 7.2 зовёт LanguageSpecific\: своя копия держит связку, которая работает.

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

mkdir /tmp/x && cd /tmp/x && echo '{}' > composer.json
composer require sbwerewolf/xml-navigator:<версия> sbwerewolf/language-specific:8.0.*
cd - && for p in xml-navigator language-specific json-serialize-trait; do
    rm -rf vendor/sbwerewolf/$p/src && cp -a /tmp/x/vendor/sbwerewolf/$p/src vendor/sbwerewolf/$p/
done
# версии — в vendor/versions.json
./build.sh --check

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

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

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

{
    "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.insync/install/js/shef-insync/ui-anchors/script.js  -> 403
/bitrix/js/shef-insync/ui-anchors/script.js                              -> 200
/bitrix/components/shef.insync/import.stat.local/class.php                -> есть на диске
/local/components/shef.insync/                                            -> нет (копия 1.x убрана)

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

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

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