Architecture
A one-page tour of what's actually in the main repo, so nothing about the generated clients feels magic.
The pipeline
download-specs.py → swaggers/*.yaml (raw upstream, checksummed)
process-specs.py → swaggers/processed/ (13 passes)
generate-clients.py <ver> → clients/<lang>/… (openapi-generator-cli, 8 langs)
├── inject-secrets.py (SecretString wrapper per lang)
├── {black|prettier|gofmt|spotless|php-cs-fixer|rustfmt} (canonicalize formatting)
└── generate-readmes.py (per-language README.md)
↓ committed at v<version>, single tag covers all 8 languages
publish.yml → PyPI / npm / Go tag / Maven Central / Packagist / hub.oscript.io / NuGet / crates.io
generate-docs.py → wb-api-client-docs (API reference: operation pages,
call snippets and data structures
in 8 languages, RU/EN)Every step is a plain Python file, so nothing is hidden behind opaque tooling.
Why generate?
WB ships OpenAPI specs. Hand-writing 13 clients × 8 languages would drift within a week. The upside of generating from the spec is that every field, every enum, every response wrapper is exactly what WB documents — and re-syncing takes exactly zero effort once the pipeline exists.
The downside is that generator output is generator output — sometimes ugly, sometimes carrying artifacts from spec quirks. Which is why there's a post-processing layer.
process-specs.py — 13 passes
inject_bearer_auth— drops WB'sHeaderApiKeyscheme (duplicated with the real bearer JWT semantics) and installs a singleBearerAuthscheme instead. Every generated client then exposes exactly one auth code path.fix_untyped_arrays— addsitems: {type: string}to arrays missing their inner type (Go / TS would otherwise emit[]any).widen_integer_formats— setsformat: int64on integer fields with no format or withuint32/uint64/Unix-timestamp formats: WB uses those for IDs and millisecond timestamps that overflow a signed 32-bit int. Explicitint32and currency-code formats are left alone.inline_top_level_arrays— inlines every$refto atype: arraycomponent and collapsesallOf: [array, description-only]wrappers (openapi-generator mishandles arrays-at-root). Runs to fixed point because inlined subtrees can contain further refs.rename_digit_prefixed_schemas—409SupplyDeliverError→Http409SupplyDeliverError(invalid Go identifier otherwise).name_inline_response_schemas— hoists anonymous inline 4xx/5xx bodies to<OperationId>Response<code>(again, valid Go identifiers).drop_response_required— WB marks nearly every response fieldrequired, but the live API omits fields at will, and every generated language then throws on deserialization. The pass deletesrequiredfrom every schema reachable from a response; request bodies keep theirrequiredlists, so input validation is unaffected.sanitize_non_ascii_enums— addsx-enum-varnamesfor Russian enum members like"Склад WB"(empty JS/Go/Java identifiers otherwise).rename_tags_to_module— WB tags are Russian (Финансовые отчёты), which openapi-generator sanitizes to an empty class-name stem, so every language fell back toApi/DefaultApiclasses. The pass rewrites every operation's tags to the module name (Finances,Reports, …), giving each module a single well-named API class (FinancesApi, …), and stashes the original tags inx-original-tagsfor the docs generator.drop_date_formats— WB returns""for unset date fields (fixTariffDateFrom,utdUcdDate), which no strict date parser accepts. The pass stripsformat: date/format: date-timefrom string schemas so clients type these as plain strings.htmlize_descriptions_to_markdown— everydescriptionfield is passed throughmarkdownifyso HTML in docstrings renders as Markdown in generated docs.absolutize_description_links— relative[…](/openapi/…)links gethttps://dev.wildberries.ruprepended so links in generated docstrings still work.add_docs_links— appends aLibrary doc: <url>line to every operation'sdescription, pointing at the operation's page on this site. Generators copydescriptioninto docstrings/Javadoc, so every client method links to its rendered docs.generate-docs.pystrips the line back out, so the docs site itself is unaffected.
generate-clients.py
- Runs
openapi-generator-cli(pinned Docker image) per spec per language, into scratch directories. - Splices each per-spec SDK into a unified per-language tree — Python packages nested under
wb_api_client.<slug>, TypeScript subpath exports, Go sub-packages, Java sub-packages, PHP sub-namespaces, for OneScript a class-name prefix (the language has no namespaces), C# namespaces, and Rust modules of a single crate (wb_api_client::<slug>). - Runs
inject-secrets.pyto patch every generatedConfiguration/ApiClientwith the language-native secret-string wrapper. - Runs the pinned formatter per language:
black,prettier,gofmt, spotless (google-java-format),php-cs-fixer,dotnet format,cargo fmt(rustfmt). All inside Docker — no host runtime needed. - Substitutes
__VERSION__in top-level manifests (pyproject.toml,package.json,go.mod,pom.xml,composer.json,Cargo.toml) with the passed version. - Runs
generate-readmes.pyto emit a per-languageREADME.md.
The result: clients/{python,typescript,go,java,php,onescript,csharp,rust}/ — eight ready-to-publish packages.
Determinism
The daily job would flap constantly if generation weren't deterministic. To keep it stable:
openapi-generator-cliDocker tag is pinned.- Every language's
openapi-generatorconfig setshideGenerationTimestamp: true. - Every formatter is pinned to a specific version.
- The PR-check workflow regenerates from scratch and fails the PR if
git diff swaggers/ clients/is non-empty. This catches any change that would make subsequent runs produce drift.
Publishing
daily-check.yml computes the version, regenerates, commits, tags. Then it fires 8 workflow_dispatch events at publish.yml — one per language — with the tag as the ref input. Each language branch of publish.yml:
- Python — OIDC → PyPI trusted publishing (
pypa/gh-action-pypi-publish). - TypeScript — OIDC → npm trusted publishing (
npm publish --provenance). - Go — the tag is pushed to
wb-api-client-go(see below); the job itself is a no-op becauseproxy.golang.orgfetches directly from the tag. - Java — GPG-signed deploy to Maven Central via
central-publishing-maven-plugin. Maven is pinned to 3.9.x (stCarolas/setup-maven): the plugin builds its bundle through a Maven-2 stager, and Maven 3.10's Resolver drops a straymaven-metadata-local.xmlinto it, so Central rejects the bundle ("Bundle has content that does NOT have a .pom file"). - PHP — ping the Packagist update-package API.
- OneScript —
opm buildthenopm pushto hub.oscript.io. - C# —
dotnet packthendotnet nuget pushto NuGet. - Rust —
cargo publishto crates.io (GitHub environmentcrates-io, toolchain pinned at 1.98).
Separately, daily-check regenerates the API reference (generate-docs.py — operation pages, call snippets, and request/response data structures per language, with links to the WB specification) and pushes it to wb-api-client-docs, which deploys to GitHub Pages.
Reusable workflows (workflow_call) would be cleaner, but PyPI + npm trusted publishing don't support them — both the OIDC token's job_workflow_ref (callee) and the Sigstore attestation cert's workflow_ref (caller) have to point at the same file, which is impossible with workflow_call. Hence workflow_dispatch.
The sibling repositories
Four languages live in their own repos, mounted here as git submodules:
ValeryVerkhoturov/wb-api-client-php→clients/php. Reason: Packagist requirescomposer.jsonat the ROOT of the crawled repo.ValeryVerkhoturov/wb-api-client-1c→clients/onescript. So 1C users can clone just the client and install it from a tag.ValeryVerkhoturov/wb-api-client-go→clients/go. Reason: Go versions modules with repository tags, so the module must sit at a repo root (go get github.com/ValeryVerkhoturov/wb-api-client-go) — a subdirectory module would needclients/go/-prefixed tags andgo getwould fetch the whole monorepo.ValeryVerkhoturov/wb-api-client-rust→clients/rust. Reason: crates.io links the crate to a dedicated repo, and git dependencies (wb-api-client = { git = "…", tag = "vX.Y.Z" }) resolve from the repo root.
Every daily run commits and tags all four siblings BEFORE the main repo — otherwise the submodule pointers recorded in the release commit would reference commits their remotes have never seen, and a fresh git clone --recurse-submodules would fail.
A fifth sibling is ValeryVerkhoturov/wb-api-client-docs — this site. Not a submodule: daily-check clones it, runs generate-docs.py, and pushes the result back.
The OneScript generator
openapi-generator ships no OneScript target, so it lives in a separate repo — ValeryVerkhoturov/onescript-openapi-generator. It is an ordinary plugin: the class extends DefaultCodegen, registers through META-INF/services, and is loaded next to openapi-generator-cli.jar, so -g onescript resolves like a built-in generator. The main repo pins it by commit — the OneScript equivalent of pinning the Docker image tag for the other languages.
What's not generated
The hand-written parts of the main repo:
scripts/— the pipeline itself, organized by bounded context underscripts/pipeline/:core/(shared kernel: repo paths, spec/module/language naming, YAML IO, external-tool runners (subprocess, docker, git, python, macOS codesign)),specs/(download, postprocess),generation/(generate, secrets),onescript/(toolchain, manifests, verify),docs/(reference, readmes). The flatscripts/*.pyfiles are thin CLI shims over those modules, named verb-object by what they produce:download-specs,process-specs,generate-clients,generate-readmes,generate-docs,generate-onescript-manifests,inject-secrets,verify-onescript. House rule: no#comments and no docstrings underscripts/— enforced bymake scripts-checkand the PR check.generator-configs/*.yaml— one openapi-generator config per language.templates/*— top-level manifests with__VERSION__placeholders..github/workflows/*— daily-check, per-language publish, PR drift check.Makefile,README.md,CLAUDE.md.
Everything under clients/ is generated output — never hand-edit it, regenerate.
Full pipeline docs
The main-repo CLAUDE.md is a working, machine-readable version of the same content — kept up to date because CI itself reads it.
