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

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

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

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

```bash
./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_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.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/`. Проверяется в самом скрипте, а не глазами.

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

```bash
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` |
| `Composer` | `composer validate --strict`: пакет ставят через Composer, и сломанный манифест виден только тому, кто ставит |
| `Skills` | `sync.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-navigator` | 7.2.9 | `SbWereWolf\XmlNavigator` |
| `sbwerewolf/language-specific` | 8.0.1 | `LanguageSpecific` |
| `sbwerewolf/json-serialize-trait` | 1.0.2 | `SbWereWolf\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\`: своя копия держит связку, которая работает.

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

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

```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.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](/modules/insync/security).

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

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