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

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

Раскладка репозитория — в [module-structure.md](/modules/problems/module-structure), процесс —
в [CONTRIBUTING.md](https://github.com/bx-shef/problems/blob/main/CONTRIBUTING.md), установка глазами пользователя —
в [README.md](/modules/problems).

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

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

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

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

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

```bash
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_php` | `php -l` по всем PHP |
| `check_short_tags` | короткий тег `<?` |
| `check_js` | `node --check` по всем JS |
| `check_lowercase` | заглавные буквы в путях `lib/` |
| `check_version` | пустой или кривой `VERSION`, пустой `VERSION_DATE` |
| `run_tests` | `tests/*_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/`. Проверяется в самом скрипте, а не глазами.

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

```bash
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` |
| `Composer` | `composer validate --strict`: пакет ставят через Composer, и сломанный манифест виден только тому, кто ставит; заодно свежесть `composer.lock` |
| `Skills` | `sync.sh --check` против `MANIFEST` источника в `bx-shef/options`: навыки здесь — копия, и копия не должна отставать |
| `Build` | `./build.sh` плюс архив артефактом прогона |
| `Lint` | `composer 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](/modules/problems/monolog).

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

```bash
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`:

```json
{
	"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](/modules/problems/security).

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

::note
Источник: [problems/docs/build-and-install.md](https://github.com/bx-shef/problems/blob/main/docs/build-and-install.md) — правки туда, сайт пересобирается сам.
::
