Архитектура
Один экран о том, что реально лежит в основном репозитории, чтобы про сгенерированные клиенты ничего не казалось магией.
Пайплайн
download-specs.py → swaggers/*.yaml (сырой апстрим, checksummed)
process-specs.py → swaggers/processed/ (13 проходов)
generate-clients.py <ver> → clients/<lang>/… (openapi-generator-cli, 8 языков)
├── inject-secrets.py (SecretString-обёртка на каждый язык)
├── {black|prettier|gofmt|spotless|php-cs-fixer|rustfmt} (канонизация форматирования)
└── generate-readmes.py (README.md на каждый язык)
↓ коммитится тегом v<version>, один тег покрывает все 8 языков
publish.yml → PyPI / npm / Go tag / Maven Central / Packagist / hub.oscript.io / NuGet / crates.io
generate-docs.py → wb-api-client-docs (справка по API: страницы операций,
сниппеты и структуры данных на 8 языках, RU/EN)Каждый шаг — обычный Python-файл, ничего не скрыто за непрозрачным тулингом.
Зачем генерировать
WB выпускает OpenAPI-спецификации. Написать 13 клиентов × 8 языков вручную — уплывут за неделю. Плюс генерации из спецификации: каждое поле, каждый enum, каждая обёртка ответа — ровно то, что задокументировал WB. И повторная синхронизация стоит нуля усилий после сборки пайплайна.
Минус — выхлоп генератора остаётся выхлопом генератора: местами уродливым, местами со следами кривостей спецификации. Именно поэтому есть слой пост-обработки.
process-specs.py — 13 проходов
inject_bearer_auth— удаляет схемуHeaderApiKey(дублирует реальный bearer JWT) и ставит однуBearerAuth. Каждый сгенерированный клиент получает ровно один путь авторизации.fix_untyped_arrays— добавляетitems: {type: string}массивам без указания внутреннего типа (иначе Go / TS сгенерируют[]any).widen_integer_formats— ставитformat: int64целым полям без формата и с форматамиuint32/uint64/Unix-timestamp: WB использует их для ID и миллисекундных меток времени, которые переполняют знаковый 32-битный int. Явныеint32и коды валют не трогаются.inline_top_level_arrays— инлайнит все$refна компоненты сtype: arrayи схлопывает обёрткиallOf: [array, description-only](openapi-generator плохо работает с массивом на корне). Гоняется до неподвижной точки, потому что заинлайненные поддеревья могут содержать другие ссылки.rename_digit_prefixed_schemas—409SupplyDeliverError→Http409SupplyDeliverError(иначе невалидный Go-идентификатор).name_inline_response_schemas— поднимает анонимные inline-тела 4xx/5xx в схемы<OperationId>Response<code>(снова ради валидных Go-идентификаторов).drop_response_required— WB помечаетrequiredпочти каждое поле ответа, но живой API опускает поля когда хочет, и тогда каждый язык падает на десериализации. Проход удаляетrequiredу всех схем, достижимых из ответов; у тел запросовrequiredсохраняется, так что валидация ввода не страдает.sanitize_non_ascii_enums— добавляетx-enum-varnamesдля русских значений вроде"Склад WB"(иначе — пустые идентификаторы в JS/Go/Java).rename_tags_to_module— теги WB русские (Финансовые отчёты), openapi-generator зачищает их до пустого имени класса, и все языки получаютApi/DefaultApi. Проход переписывает теги каждой операции на имя модуля (Finances,Reports, …), поэтому у модуля один нормально названный класс (FinancesApi, …), а исходные теги сохраняются вx-original-tagsдля генератора документации.drop_date_formats— WB возвращает""для неустановленных дат (fixTariffDateFrom,utdUcdDate), что не парсится ни одним строгим date-парсером. Проход снимаетformat: date/format: date-timeсо строковых схем, и клиенты типизируют их как обычные строки.htmlize_descriptions_to_markdown— каждое полеdescriptionпрогоняется черезmarkdownify, чтобы HTML в докстрингах рендерился как Markdown.absolutize_description_links— к относительным ссылкам[…](/openapi/…)добавляетсяhttps://dev.wildberries.ru, чтобы ссылки в сгенерированных докстрингах открывались.add_docs_links— добавляет строкуLibrary doc: <url>вdescriptionкаждой операции со ссылкой на страницу операции на этом сайте. Генераторы копируютdescriptionв докстринги/Javadoc, поэтому каждый метод клиента ссылается на свою страницу документации.generate-docs.pyвырезает эту строку обратно, так что сам сайт не затрагивается.
generate-clients.py
- Гоняет
openapi-generator-cli(закреплённый Docker-образ) для каждой спецификации и каждого языка во временные каталоги. - Склеивает пер-спецификационные SDK в единое дерево на язык — Python-пакеты под
wb_api_client.<slug>, TypeScript subpath-экспорты, Go под-пакеты, Java под-пакеты, PHP под-пространства, OneScript — по префиксу имени класса (пространств имён в языке нет), C# — пространства имён, Rust — модули одного крейта (wb_api_client::<slug>). - Запускает
inject-secrets.py, чтобы патчить каждый сгенерированныйConfiguration/ApiClientсоответствующей языковой обёрткой секрета. - Запускает закреплённый форматтер:
black,prettier,gofmt, spotless (google-java-format),php-cs-fixer,dotnet format,cargo fmt(rustfmt). Всё внутри Docker — без языковых рантаймов на хосте. - Подставляет
__VERSION__в топ-уровневые манифесты (pyproject.toml,package.json,go.mod,pom.xml,composer.json,Cargo.toml). - Запускает
generate-readmes.py, чтобы выпустить README на каждый язык.
Итог: clients/{python,typescript,go,java,php,onescript,csharp,rust}/ — восемь пакетов, готовых к публикации.
Детерминизм
Ежедневная задача постоянно бы «дрожала», не будь генерация детерминированной. Что для этого сделано:
- Docker-тег
openapi-generator-cliзакреплён. - В конфиге openapi-generator для каждого языка выставлено
hideGenerationTimestamp: true. - Все форматтеры закреплены на конкретных версиях.
- PR-check-workflow перегенерирует всё с нуля и падает, если
git diff swaggers/ clients/непустой. Это ловит любое изменение, из-за которого следующие прогоны разъедутся.
Публикация
daily-check.yml считает версию, регенерирует, коммитит, тегает. Затем шлёт 8 событий workflow_dispatch в publish.yml — по одному на язык — с тегом в качестве входа ref. Каждая языковая ветка publish.yml:
- Python — OIDC → PyPI trusted publishing (
pypa/gh-action-pypi-publish). - TypeScript — OIDC → npm trusted publishing (
npm publish --provenance). - Go — тег пушится в
wb-api-client-go(см. ниже), сама джоба — no-op:proxy.golang.orgтянет прямо из тега. - Java — подписанный GPG-деплой в Maven Central через
central-publishing-maven-plugin. Maven закреплён на 3.9.x (stCarolas/setup-maven): плагин собирает bundle через Maven-2 stager, и Resolver из Maven 3.10 роняет туда служебныйmaven-metadata-local.xml, из-за чего Central отклоняет bundle («Bundle has content that does NOT have a .pom file»). - PHP — пинг API
packagist.org/api/update-package. - OneScript —
opm buildиopm pushв hub.oscript.io. - C# —
dotnet packиdotnet nuget pushв NuGet. - Rust —
cargo publishв crates.io (GitHub-окружениеcrates-io, toolchain закреплён на 1.98).
Отдельно daily-check перегенерирует справку по API (generate-docs.py — страницы операций, сниппеты вызова и структуры входящих/исходящих данных на каждом языке, ссылки на спецификацию WB) и пушит её в wb-api-client-docs, откуда сайт деплоится на GitHub Pages.
Reusable-воркфлоу (workflow_call) был бы аккуратнее, но PyPI и npm trusted publishing их не поддерживают: и OIDC-claim job_workflow_ref (callee), и Sigstore-attestation workflow_ref (caller) должны указывать на один и тот же файл, что с workflow_call невозможно. Поэтому — workflow_dispatch.
Соседние репозитории
Четыре языка живут в своих репозиториях, подключённых сюда как git-submodule:
ValeryVerkhoturov/wb-api-client-php→clients/php. Причина: Packagist требуетcomposer.jsonв КОРНЕ обходимого репозитория.ValeryVerkhoturov/wb-api-client-1c→clients/onescript. Так пользователи 1С могут склонировать только клиент и поставить его из тега.ValeryVerkhoturov/wb-api-client-go→clients/go. Причина: Go версионирует модули тегами репозитория, поэтому модуль обязан лежать в корне репо (go get github.com/ValeryVerkhoturov/wb-api-client-go) — модуль в подкаталоге потребовал бы тегов видаclients/go/v…и тянул бы весь монорепозиторий.ValeryVerkhoturov/wb-api-client-rust→clients/rust. Причина: crates.io связывает крейт с выделенным репозиторием, а git-зависимости (wb-api-client = { git = "…", tag = "vX.Y.Z" }) резолвятся от корня репозитория.
Ежедневный прогон коммитит и тегает все четыре соседних репозитория ДО основного — иначе записанные указатели сабмодулей ссылались бы на коммиты, которых на их удалённых ещё нет, и свежий git clone --recurse-submodules падал бы.
Пятый сосед — ValeryVerkhoturov/wb-api-client-docs, этот сайт. Он не сабмодуль: daily-check клонирует его, запускает generate-docs.py и пушит результат обратно.
Генератор OneScript
В openapi-generator нет генератора OneScript, поэтому он вынесен в отдельный репозиторий — ValeryVerkhoturov/onescript-openapi-generator. Это обычный плагин: класс наследует DefaultCodegen, регистрируется через META-INF/services и подключается рядом с openapi-generator-cli.jar, так что -g onescript работает как встроенный генератор. В основном репозитории он закреплён по коммиту — это аналог закреплённого тега Docker-образа для остальных языков.
Что НЕ генерируется
Ручные части основного репозитория:
scripts/— сам пайплайн, разложенный по доменным контекстам вscripts/pipeline/:core/(общее ядро: пути репозитория, именование спецификаций/модулей/языков, YAML IO, запуск внешних инструментов (subprocess, docker, git, python, macOS codesign)),specs/(download, postprocess),generation/(generate, secrets),onescript/(toolchain, manifests, verify),docs/(reference, readmes). Плоскиеscripts/*.py— тонкие CLI-обёртки над этими модулями, названные по производимому результату:download-specs,process-specs,generate-clients,generate-readmes,generate-docs,generate-onescript-manifests,inject-secrets,verify-onescript. Домашнее правило: ни#-комментариев, ни докстрингов подscripts/— проверяется вmake scripts-checkи в PR-check.generator-configs/*.yaml— по одному конфигу openapi-generator на язык.templates/*— топ-уровневые манифесты с плейсхолдерами__VERSION__..github/workflows/*— daily-check, per-language publish, PR drift check.Makefile,README.md,CLAUDE.md.
Всё под clients/ — сгенерированный выхлоп, руками не редактируется. Перегенерируйте.
Полное описание пайплайна
Файл CLAUDE.md в основном репозитории — рабочая, машиночитаемая версия того же содержимого, поддерживается в актуальном состоянии.
