Монорепо плагинов AnimeDB v2 (официальных и принятых от сообщества) и одновременно реестр маркета.
anime-db-plugins/
├── plugins/
│ └── <plugin-id>/ # один каталог = один плагин
│ ├── manifest.json # описание плагина (обязательно в корне)
│ ├── src/ # PHP-классы — у плагинов с кодом (integration/local)
│ ├── assets/ # статика (CSS/JS/картинки/шрифты), которую хост может подключить
│ └── translations/ # каталоги переводов — обязательны у translation
│ └── native/ # необязательный каталог переводов нативного слоя приложения
└── plugins-registry.json # агрегат реестра (генерируется CI)
Плагин типа translation — декларативный ресурс без кода: у него нет src/ вовсе, и
это не упущение, а требование валидатора.
Id плагина имеет вид <vendor>-<name>, где <vendor> — префикс вендора
(автора): он обеспечивает уникальность id и защищает имя от занятия другими.
Официальные плагины идут с вендором animedb (например animedb-shikimori);
плагины сообщества — со своим вендором (например johnsmith-example). Вендор
animedb зарезервирован за официальными плагинами; для своих плагинов авторы
используют собственный вендор.
Namespace классов выводится из id детерминированно (каждый сегмент id — в
StudlyCase, слитно, под общим префиксом AnimeDb\Plugins\):
animedb-shikimori→AnimeDb\Plugins\AnimedbShikimori\johnsmith-example→AnimeDb\Plugins\JohnsmithExample\
Префикс AnimeDb\Plugins\ — общий для всех плагинов, включая комьюнити (вендор
влияет только на сегмент studly, не на корень).
Здесь перечислены официальные плагины; полный список, включая плагины сообщества, — в
plugins-registry.json.
| Плагин | Тип | Назначение |
|---|---|---|
animedb-anidb |
integration |
Источник метаданных AniDB: поиск по названию, распознавание ссылок, заполнение карточки, виджеты |
animedb-folder-info |
local |
Описание записи и обложка в папке записи: animedb-info.html автоматически и по кнопке |
animedb-language-pack |
translation |
Языковой пакет: языки интерфейса de, ja |
animedb-link-rules |
local |
Правила нормализации ссылок справочных сайтов для сравнения записей каталога |
animedb-media-details |
local |
Характеристики файлов записи: размер, длительность, видео, звуковые дорожки, языки звука и субтитров |
animedb-myanimelist |
integration |
Источник метаданных MyAnimeList: поиск, распознавание ссылок, заполнение карточек, синхронизация, виджеты |
animedb-shikimori |
integration |
Источник метаданных Shikimori: поиск, распознавание ссылок, заполнение карточек, синхронизация, виджеты |
animedb-wikidata |
integration |
Мост связывания через Wikidata: по ссылке IMDb/Кинопоиска добавляет ссылки на MAL, AniDB, ANN, AniList |
animedb-worldart |
integration |
Источник метаданных World-Art: поиск по названию, распознавание ссылок, заполнение записи |
Колонка «Тип» — это поле type манифеста, и оно определяет не только назначение, но и
правила валидации: у translation не должно быть src/, зато обязателен translations/
и непустой locales; домен каталога переводов тоже зависит от типа (см.
.claude-docs/conventions.md).
Плагин типа translation дополнительно обязан объявить в манифесте
translation_keys_count — целое число, равное фактическому числу листовых ключей его
каталога translations/messages.<locale>.yaml (число ключей одной локали, поскольку
валидатор уже требует их совпадения между локалями плагина). Поле нужно витрине маркета,
чтобы показать полноту перевода без скачивания ZIP; для integration/local оно
запрещено, как и locales. Подробности и формула подсчёта — в
.claude-docs/conventions.md.
В plugins-registry.json это же число попадает в запись версии (versions[], рядом с
core/sha256), а не в блок manifest: у resolveCompatibleVersion() приложения нет
гарантии, что установится именно последняя версия, поэтому счётчик должен описывать
конкретный опубликованный артефакт, а не рабочее дерево master. Источник —
translation_keys_count из релизного ассета manifest.json, который шаг Collect published versions в registry.yml уже скачивает по тегу ради require.core/sha256.
Поле опциональное: у версий, опубликованных до его появления, оно отсутствует и появиться
не может, так как ассеты релизов иммутабельны.
Плагин с кодом (integration, local) — это manifest.json + src/*.php (local с
link_rules: true может состоять только из manifest.json + link-rules.yaml + обязательного
link-rules.examples.yaml с примерами для проверки правил, без src/), без
собственного DI-конфига, без класса бандла и без тегов: всё это хост-приложение
настраивает само (регистрация сервисов, тегирование по контрактным интерфейсам,
автозагрузка изолированного namespace). Контракт (anime-db/plugin-contracts) в
рантайме предоставляет хост, поэтому vendor/ в ZIP-сборку плагина не входит.
В корне плагина любого типа обязателен непустой README.md (проверяются только наличие и
непустота после обрезки пробельных символов, содержимое не анализируется).
Плагин типа translation кода не имеет вовсе — это manifest.json + translations/.
Хост не регистрирует ему ни сервисов, ни маршрутов: он только подключает каталоги
переводов и расширяет список доступных локалей.
Плагин типа translation может дополнительно везти translations/native/<locale>.json —
перевод нативного слоя приложения (сплэш-экран, меню трея, диалоги отказа запуска), который
живёт отдельно от каталога translations/messages.<locale>.yaml, описанного выше. Каталог
опциональный: у большинства языковых пакетов его нет, и это нормально.
- Формат — плоский JSON-объект
строка → строка. Вложенность запрещена: нативный слой обращается к ключу простымcatalog[key]и не найдёт вложенное значение. - Формат именно JSON, а не YAML: у нативного слоя нет рантайм-зависимостей и YAML-парсера.
- Каждая локаль, для которой есть файл в
translations/native/, обязана быть объявлена вlocalesманифеста. Обратное не требуется: локаль вlocalesбез соответствующего файла вtranslations/native/— валидна, поскольку сам каталог необязателен. - Действуют те же контентные правила, что и для
messages.<locale>.yaml: пустое значение запрещено, плейсхолдеры — только вида%name%, символ|запрещён. Паритет ключей и плейсхолдеров требуется между локалями, но только внутри самогоtranslations/native/. - Ключи
translations/native/не входят вtranslation_keys_count— это поле по-прежнему считает только листовые ключиtranslations/messages.<locale>.yaml. - Пространство имён ключей
translations/native/не пересекается сmessages: совпадение имени ключа между ними не является ошибкой.
Формат — translations/native/<locale>.json (подкаталог), а не плоский файл
translations/native.<locale>.json: хост отдаёт каталог translations/ каждого плагина в
пути переводов Symfony, и имя файла с двумя точками было бы подхвачено как отдельный домен
переводов, которым оно не является.
Плагин со своей вёрсткой (стили страницы настроек, скрипт виджета и т.п.) кладёт эти файлы
в assets/ и объявляет их в manifest.json:
"ui": { "css": ["assets/carousel.css"], "js": ["assets/settings.js"] }Хост подключает перечисленные там файлы там, где сам рисует шелл страницы (страница
настроек плагина, страница с его виджетами) — подключать их из разметки плагина в этих
местах не нужно. На собственных страницах плагина и в ответах его собственных маршрутов
(например, страница результата OAuth со своим <head>) шелл хоста не участвует, и теги
<link>/<script> там ставит сам плагин. Поле
ui доступно с anime-db/plugin-contracts v0.19, поэтому плагин, объявивший ui,
обязан пинить require.plugin-contracts диапазоном, покрывающим 0.19 (например
^0.19) — иначе он обещает хост, который это поле ещё не умеет читать.
Картинки в ui не объявляются: разметка плагина подключает их сама через Twig-функцию
хоста plugin_asset(), либо CSS плагина ссылается на них относительным url(...).
Любой файл в assets/ (объявлен он в ui или нет) обязан иметь одно из расширений,
которые хост реально отдаёт: .css, .js, .svg, .png, .webp, .woff2. Файл с
другим расширением всё равно уедет в ZIP плагина (assets/ не входит в список
исключений сборщика), но ни один маршрут хоста его не отдаст — мёртвый груз.
tools/src/PluginValidator.php проверяет и существование объявленных в ui файлов
внутри каталога плагина, и то, что их путь не выходит за пределы assets/, и
аллоу-лист расширений для всего содержимого assets/ — но не содержимое самих файлов
(ни линтера JS, ни разбора CSS).
Виджеты плагина приезжают в страницу хоста уже после её загрузки (через htmx-свопы), а
не присутствуют в ней при DOMContentLoaded. JS плагина обязан это учитывать: обработчики
вешаются делегированием на document (document.addEventListener('click', e => { if (e.target.matches('.my-widget button')) { ... } })) или по событию htmx:load, а не
одноразовым обходом DOM на DOMContentLoaded — такой обход просто не увидит виджет,
подставленный позже.
Готовые сборки версий плагинов публикуются в GitHub Releases, а plugins-registry.json
в корне репозитория агрегирует реестр для витрины в приложении. Инфраструктура
сборки/реестра (CI, генерация реестра, зеркала) — в разработке.
Реестр содержит поле sequence — монотонный счётчик поколений (anti-rollback): клиент
отвергает реестр с sequence, меньшим уже увиденного. tools/build-registry.php выводит
его как previous.sequence + 1, если ему передан путь к предыдущему опубликованному
plugins-registry.json (третий аргумент). Если этот аргумент вовсе не передан —
sequence стартует с 1 (первая генерация). Если аргумент передан, но по этому пути
файла нет, сборка завершается с ошибкой, а не молча стартует счётчик заново. Полей
valid_until/freshness в реестре сознательно нет — см. issue #12.
Реестр подписывается отсоединённой Ed25519-подписью (tools/sign-registry.php) —
дешёвая страховка от компрометации хостинга-зеркала: plugins-registry.json.sig
проверяется sodium_crypto_sign_verify_detached против публичного ключа (tools/verify-registry-signature.php).
Приватный ключ никогда не в git — он живёт только как секрет GitHub Actions и
передаётся sign-registry.php через переменную окружения (имя которой — второй
аргумент), не через argv. Публичный ключ, наоборот, безопасно коммитить в репозиторий —
он предназначен для зашивки в клиент (issue #292).
Сгенерировать новую пару ключей (разово, при первичной настройке или ротации):
php tools/generate-registry-keypair.php plugins-registry.pub
# stdout — секретный ключ (base64): положить в секрет GitHub Actions (например
# REGISTRY_SIGNING_KEY) и не сохранять больше нигде.Собрать и подписать реестр вручную (то, что должен делать CI-воркфлоу — здесь не
описан, т.к. .github/workflows/ вне зоны ответственности этого README):
php tools/build-registry.php plugins published-versions.json plugins-registry.json \
> plugins-registry.json.new
mv plugins-registry.json.new plugins-registry.json
REGISTRY_SIGNING_KEY="$(cat secret.key)" \
php tools/sign-registry.php plugins-registry.json REGISTRY_SIGNING_KEY \
> plugins-registry.json.sigПлагины сообщества попадают в маркет через Pull Request в этот монорепо (со своим вендором в id) — так их можно проверить и собрать общим CI. Автор может и не публиковать плагин в маркете, а распространять свой ZIP самостоятельно: тогда пользователь ставит его как сторонний архив (минуя маркет), а не из витрины.
Ассеты версии (plugin.zip, manifest.json) раздаются клиенту с зеркал, а не только
из GitHub Releases. GitHub Releases — хардкод-константа
(AssetMirrorsResolver::GITHUB_MIRROR) и всегда первый элемент asset_mirrors: у
него нет FTP-кредов (заливка через GH_TOKEN, вне MIRROR_CREDS), это вечный
источник-истины, и первым местом в списке он задаёт клиенту детерминированный порядок
фолбэка (GitHub → реплики). Каждая реплика после него собирается из двух мест —
это и есть единый источник координат зеркала (issue #26):
-
MIRROR_CREDS(GitHub Secret, JSON-объект, ключ — id зеркала) — и write-креды, и публичный URL-шаблон одной записью:{ "mirror1": { "host": "ftp.example.tld", "port": 21, "user": "mirror_user", "password": "***", "dir": "/public_html/mirror", "protocol": "ftps", "public_url": "https://mirror1.example.org/mirror/<id>/<version>/<file>" } }Поля:
host,port(опц., по умолчанию 21),user,password,dir(корневой каталог на зеркале, абсолютный путь — с ведущим/),protocol(ftps— по умолчанию и предпочтительно,ftp— фолбэк для хостинга без FTPS),public_url— URL-шаблон с макросами<id>/<version>/<file>, под которым зеркало отдаёт ассеты клиенту.public_urlобязателен структурно (непустая строка), но его форма (https://+ все три макроса) не проверяется при парсингеMirrorCredentialsParser— это делаетAssetMirrorsResolverпри сборкеasset_mirrors(см. ниже), и делает это fail-open, а не throw. -
active-mirrors(файл в корне репозитория, git-tracked, ревьюится PR'ами) — список id зеркал, один на строку (пустые строки и#-комментарии игнорируются):mirror1Зеркало попадает в
asset_mirrors, только если его id одновременно есть и вMIRROR_CREDS, и вactive-mirrors. Это разделяет две разные категории решений: что реклама клиентам (безопасно-значимо → ревьюится в git) и какие у зеркала координаты (секрет, без PR-гейта, одна правка задаёт и куда лить, и откуда качать). Де-листинг забаненного/протёкшего зеркала — это просто «убрать id изactive-mirrors» и пересобрать реестр.
AssetMirrorsResolver::resolve() собирает итоговый asset_mirrors = [GitHub] + активные реплики и fail-open: id из active-mirrors без соответствующей записи в
MIRROR_CREDS, или запись с невалидным public_url (не https://, или отсутствует
хотя бы один из макросов <id>/<version>/<file>), — выбрасываются из результата с
предупреждением (::warning::), а не валят сборку: MIRROR_CREDS — секрет без
PR-ревью, и опечатка в одной реплике не должна морозить пересборку/подпись реестра для
всех плагинов. Пустой active-mirrors → asset_mirrors == [GitHub].
tools/build-registry.php подключает этот механизм двумя необязательными аргументами —
без них поведение не меняется (fallback на GitHub-only, как до issue #26):
php tools/build-registry.php plugins published-versions.json [<previous-registry.json>] \
<active-mirrors-file> <mirror-creds-env-var-name>tools/push-mirror-assets.php читает JSON MIRROR_CREDS из
переменной окружения (имя — четвёртый аргумент, не argv — секрет не должен попадать
в список процессов) и в цикле по всем ключам заливает plugin.zip и
manifest.json версии на каждое зеркало по FTP/FTPS:
MIRROR_CREDS='...' php tools/push-mirror-assets.php <id> <version> <dir-с-ассетами> MIRROR_CREDSРаскладка на зеркале — версионное неизменяемое дерево <dir>/<id>/<version>/<file>
(тот же <id>/<version>/<file>, что и в asset_mirrors). Файлы заливаются с
перезаписью: версия неизменяема, поэтому повторная заливка байт-в-байт идентична, а
перезапись самоисцеляет файл, усечённый оборванной загрузкой (пропуск «уже есть»
оставил бы битый ассет навсегда). Безопасно перезапускать на уже опубликованную
версию. Если MIRROR_CREDS не задана или пуста —
это не ошибка, а «зеркала ещё не настроены»: скрипт завершается кодом 0, ничего не делая.
После успешной заливки на зеркало push-mirror-assets.php HEAD-проверяет public_url
каждого залитого файла (MirrorAssetReachabilityVerifier + HttpMirrorReachabilityChecker
— HEAD с ретраем/бэкоффом, фолбэк на ranged GET при 405, поскольку часть
шаред-хостингов запрещает HEAD). В отличие от самой заливки (падение = код ≠0, релиз
не идёт дальше) HEAD-провал здесь — soft-fail: печатается громкая
::warning::-аннотация, но скрипт завершается кодом 0 и релиз продолжается — GitHub
(asset_mirrors[0]) остаётся доступен клиенту независимо от состояния реплики, а
пропагация FTP→web на шаред-хостинге может отставать на несколько секунд.
Добавить зеркало = одна правка секрета (MIRROR_CREDS) + одна правка в git
(active-mirrors, через бэкофилл-джоб — см. ниже), не код. Число секретов не растёт
с числом зеркал.
Порядок публикации — инвариант: реестр (sequence) ссылается на sha256 уже
залитых ассетов, поэтому ассеты должны лечь на все активные зеркала раньше, чем
plugins-registry.json будет пересобран/подписан/закоммичен — иначе клиент может
увидеть в реестре версию, ассетов которой на зеркале ещё нет. push-mirror-assets.php
падает с кодом ≠0 при первой же неудаче ЗАЛИВКИ (сбой коннекта/логина/загрузки на любое
зеркало; HEAD-провал сюда не относится, см. выше) — вызывающий (CI) обязан трактовать
это как «реестр не публикуем». Сам скрипт не решает, когда его вызвать относительно
сборки реестра — эта последовательность (и переброс FTP-шага в CI) специфична для
.github/workflows/, вне зоны ответственности этого README (см. соответствующий
комментарий выше про build-registry.php/sign-registry.php).
Новое (ещё не активное) зеркало не появляется в asset_mirrors, пока не пройдёт
бэкофилл — tools/backfill-mirror.php (MirrorBackfillPublisher):
MIRROR_CREDS='...' php tools/backfill-mirror.php <mirror-id> MIRROR_CREDS <active-mirrors-file>Скрипт читает все теги Release через gh (топология «звезда»: GitHub — хаб,
зеркала — спицы; никогда не тянет с другого зеркала по FTP — read-креды зеркал не
нужны в принципе), заливает исторические ассеты каждой версии на целевое зеркало,
HEAD-верифицирует каждый залитый файл и — только на полном успехе всех версий —
дописывает <mirror-id> в <active-mirrors-file> (файл переписывается
отсортированным/дедуплицированным; при любой ошибке остаётся нетронутым). В отличие от
релизного HEAD-чека (soft-fail, см. выше) здесь HEAD — hard-fail: это не релиз под
давлением времени, поэтому безопасно (и правильно) отказаться активировать зеркало,
которое не может подтверждённо отдать то, что на него только что залили. Тот же джоб
служит само-исцелением: если зеркало «поплыло» (частичный вайп/prune хостингом),
повторный запуск на уже активном id пере-проецирует всё из GitHub заново (заливка
идемпотентна — см. выше про перезапись). Сам скрипт не коммитит active-mirrors в git —
это, как и вызов на workflow_dispatch, задача CI-обвязки (см. ниже).
Что уже вынесено в этот релиз, а что — нет.
.github/workflows/в этой задаче не менялся (правки туда не входили в согласованный объём этого PR):public_url/active-mirrors/AssetMirrorsResolver/MirrorBackfillPublisher/HEAD-verify — весь код и вся тестируемая логика уже здесь и покрыты тестами, а вот собственноworkflow_dispatch-триггеры (registry.yml, бэкофилл-джоб) и проброс новых аргументовbuild-registry.php/шагgit commit active-mirrorsв существующие воркфлоу — ещё предстоит добавить отдельно, тем, у кого есть право трогать.github/workflows/.
Реестр на зеркалах. Само plugins-registry.json (+ отсоединённая подпись
plugins-registry.json.sig) тоже льётся в корень каждого зеркала
(<dir>/plugins-registry.json), чтобы зеркало было самодостаточным источником:
клиент тянет и реестр, и ассеты с одного хоста. tools/push-mirror-registry.php
читает тот же MIRROR_CREDS и в цикле по зеркалам заливает оба файла:
MIRROR_CREDS='...' php tools/push-mirror-registry.php <registry.json> <signature.json.sig> MIRROR_CREDSВ отличие от ассетов версии (неизменяемое дерево, никогда не перезаписывается) реестр
мутабелен — меняется на каждый релиз (новый sequence/подпись) — поэтому оба
файла перезаписываются. Реестр заливается перед подписью (registry раньше
.sig): в короткое окно между двумя загрузками клиент видит свежий реестр против
старой подписи, проверка падает, и он остаётся на последнем валидном реестре из кэша —
безопасная деградация, а не доверие рассогласованию. Как и у ассетов, пустой/незаданный
MIRROR_CREDS → код 0 без действий. Заливка идёт после подписи и до коммита
реестра в master (провал → нет коммита → ретрай на следующем прогоне; последовательность
живёт в .github/workflows/, вне этого README).
Каркас проверки плагинов на PHP, самодостаточно вызываемый из CLI:
composer install
# Проверить один плагин: манифест, соответствие id каталогу, namespace классов
# в src/, синтаксис PHP. Код возврата 0 — плагин валиден, иначе печатает список проблем.
php tools/validate-plugin.php plugins/animedb-shikimori
# Гейт «PR трогает ровно один плагин и только его файлы». На вход — список путей
# (аргументами или через stdin, например из `git diff --name-only`), на выход —
# id единственного затронутого плагина (код 0) либо причина отказа (код ≠0).
git diff --name-only master... | php tools/check-pr-changes.php
# Гейт «версия поднята». На вход — список путей PR через stdin, аргументом — база
# сравнения (ref/sha, из которого читается base-версия манифеста через `git show`).
# Для каждого затронутого плагина, у которого изменились файлы из дистрибутива
# (см. tools/src/PublishedContentRules.php — тот же список, что и у build-plugin-zip),
# требует version > base-версии И отсутствия уже существующего тега <id>/<version>
# (проверяется через `git ls-remote --exit-code`, авторизация не нужна — репозиторий
# публичный; сбой самой проверки, например сети, тоже считается нарушением, а не
# пропуском). Новый или полностью удалённый этим PR плагин, а также правки только
# tests/ — пропускаются.
git diff --name-only master... | php tools/check-version-bump.php master
# Собрать детерминированный ZIP плагина (manifest.json в корне, src/, README.md;
# без vendor/, composer.lock, tests/, .git*, .php-cs-fixer.*) и посчитать sha256.
# Хеш печатается в stdout и кладётся рядом с архивом в файл "sha256".
php tools/build-plugin-zip.php plugins/animedb-shikimori /tmp/animedb-shikimori.zip
# Собрать plugins-registry.json (см. "Маркет и версии" выше) и подписать его Ed25519.
# <active-mirrors-file>/<mirror-creds-env-var-name> опциональны: без них asset_mirrors —
# GitHub-only (как до issue #26). С ними — [GitHub] + активные реплики (см. "Зеркала").
php tools/build-registry.php plugins published-versions.json [<previous-registry.json>] \
[<active-mirrors-file> <mirror-creds-env-var-name>]
php tools/sign-registry.php <registry.json> <secret-key-env-var-name>
php tools/verify-registry-signature.php <registry.json> <signature-file> <public-key-file>
php tools/generate-registry-keypair.php <public-key-out-file>
# Залить ассеты версии на все зеркала из MIRROR_CREDS (см. "Зеркала" выше). Код 0 без
# заливки, если переменная не задана/пуста или декодируется в пустой JSON-объект —
# зеркала опциональны. Код ≠0 при сбое ЗАЛИВКИ на любое зеркало (HEAD-проверка после
# заливки — soft-fail, только предупреждение, не влияет на код возврата).
php tools/push-mirror-assets.php <id> <version> <dir-с-ассетами> <mirror-creds-env-var-name>
# Залить подписанный реестр (plugins-registry.json + .sig) в корень каждого зеркала из
# MIRROR_CREDS — перезапись (реестр мутабелен). Те же коды возврата, что у ассетов.
php tools/push-mirror-registry.php <registry.json> <signature-file> <mirror-creds-env-var-name>
# Активировать/бэкофилльнуть зеркало: залить ВСЕ исторические версии из GitHub Releases
# на <mirror-id> и, только при полном успехе (заливка + HEAD-верификация каждой), дописать
# id в <active-mirrors-file> (см. "Активация зеркала" выше). HEAD здесь — hard-fail.
php tools/backfill-mirror.php <mirror-id> <mirror-creds-env-var-name> <active-mirrors-file>
# Сгенерировать markdown-тело релиза одного плагина — по аналогии с кнопкой GitHub
# «Generate release notes», но с фильтрацией по plugins/<id>/ (монорепо с несколькими
# плагинами и общей инфраструктурой) и с авторитетным commit→PR через `gh api
# .../commits/<sha>/pulls`, а не регуляркой по "#NN" в subject (см. issue #28: в этом
# репозитории "#NN" в subject часто ссылается на issue, а не на PR). Чистый инструмент:
# без сети и без git внутри себя, сбор сырья и вызов из релизного воркфлоу — за ментейнером.
# 1) выбрать предыдущий тег плагина (семантическое сравнение версий, не sort -V):
git tag --list 'animedb-shikimori/*' | php tools/build-release-notes.php pick-prev 0.2.0
# 2) отформатировать тело релиза из JSON коммитов диапазона (с полем "prs" на коммит):
echo "$COMMITS_JSON" | php tools/build-release-notes.php format \
--id animedb-shikimori --version 0.2.0 --repo anime-db/anime-db-plugins \
[--prev animedb-shikimori/0.1.0]
composer test # PHPUnit
composer phpstan # статический анализ
composer cs-check # проверка стиля кода (php-cs-fixer, --dry-run)Все скрипты — чистый CLI (вход → вывод/код возврата), без обвязки CI: воркфлоу
.github/workflows, который их дёргает (git diff → check-pr-changes → validate-plugin →
build-plugin-zip + создание Release на затронутом плагине), интегрирует ментейнер отдельно.
anime-db/plugin-contracts устанавливается с Packagist (https://packagist.org/packages/anime-db/plugin-contracts).
Пакет публичный — composer install работает без аутентификации и без токена, как
локально, так и в CI (в т.ч. на PR из форков).
GNU General Public License v3.0 or later (GPL-3.0-or-later). Полный текст —
в файле LICENSE; он же кладётся в корень каждого собираемого ZIP плагина.