Monorepo Structure and Release Process
Explains how the polyglot sdks repository is laid out, how the shared OpenAPI and JSON Schema contracts flow into each language's generated code, and how each package is released independently.
The sdks repository is a single polyglot monorepo: one Node.js SDK, one Python SDK, one Go SDK, and one Node-based CLI, all generated from two shared contracts instead of hand-maintained independently in each language.
Repository layout#
The repo keeps the two shared contracts at the root and one directory per shipped package:
sdks/
.github/workflows/
openapi/
json-schema/
sdk-go/
sdk-node/
sdk-python/
cli-node/
packages/api-core/
test-fixtures/
openapi/primitive-api.yaml: the hand-written OpenAPI 3.1 spec, the single source of truth for the HTTP API surface consumed by every SDK and the CLI.json-schema/email-received-event.schema.json: the canonical JSON Schema for the inbound webhook payload (theemail.received/EmailReceivedEventshape).sdk-node/,sdk-python/,sdk-go/: the three language SDKs.cli-node/: theprimitiveCLI, built on the Node SDK andpackages/api-core.packages/api-core/: the workspace-internal, never-published TypeScript package holding the generated OpenAPI client, the operation manifest, andPrimitiveApiClient. See What is API Core?.test-fixtures/: shared behavioral-parity fixtures (schema validation outcomes, signature verification, auth classification, sender trust, forward compatibility) that all three SDKs must pass identically.
The two shared contracts#
Every SDK is responsible for the same behavior, driven by the same two files:
| Contract | File | Drives |
|---|---|---|
| Webhook payload shape | json-schema/email-received-event.schema.json | Typed EmailReceivedEvent models, runtime schema validation, forward-compatibility handling of unknown event types |
| HTTP API surface | openapi/primitive-api.yaml | Generated typed clients, the operation manifest, request/response types |
For the webhook side, each SDK independently:
- verifies the
Primitive-Signatureheader - parses the raw request body
- validates the payload against the canonical schema
- exposes a typed
email.receivedevent model - preserves forward compatibility for event types the SDK doesn't recognize yet
The full webhook contract (signature format, event catalog, forward-compatibility guarantees) is explained once, in Webhook Events Overview; the normalized ReceivedEmail object built on top of it is covered in Inbound and Outbound Email Model.
For the API side, openapi/primitive-api.yaml is authored as OpenAPI 3.1. It is never edited by generators; instead it is normalized into a build artifact, primitive-api.codegen.json (OpenAPI 3.0.3), that the three language-specific code generators actually consume. That normalization step, the per-language generator pipelines, and the api-core package's role are explained in Architecture: Shared Codegen Pipeline and OpenAPI Spec Normalization and Codegen Artifacts. Running the full regeneration pipeline end to end is covered in Regenerating SDK Code from the OpenAPI Spec.
Author and edit openapi/primitive-api.yaml directly. Never hand-edit primitive-api.codegen.json; it's a generated build artifact regenerated by the api-core scripts.
SDK layers#
Each SDK layers the same generated artifacts on top of the same two contracts, but exposes them through idiomatic language surfaces:
| SDK | Package path | Published as | Generated artifacts |
|---|---|---|---|
| Node | sdk-node/ | @primitivedotdev/sdk (root, webhook, api, openapi, contract, parser subpaths) | AJV standalone validator, TypeScript types, generated API client, OpenAPI document export |
| Python | sdk-python/ | primitivedotdev (import name primitive) | Packaged webhook schema copy, generated webhook models, generated API client under primitive.api |
| Go | sdk-go/ | github.com/primitivedotdev/sdks/sdk-go | Embedded webhook schema source, generated API client under sdk-go/api |
| CLI | cli-node/ | primitive (npm) | Bundles the Node SDK and api-core inline |
Node additionally ships contract and parser as Node-only subpaths (not exposed in Python or Go).
Change strategy#
When changing shared webhook behavior:
- Update
json-schema/email-received-event.schema.jsonif the payload contract itself changes. - Regenerate language-specific artifacts for all three SDKs.
- Update
test-fixtures/when the expected cross-language behavior changes. - Run
make check. - Review each SDK for language-specific helper implications.
When changing the HTTP API contract:
- Update
openapi/primitive-api.yaml. - Regenerate the Node, Python, and Go API clients.
- Run
make node-check python-check go-check. - Verify the Node smoke test still exposes
apiandopenapisubpaths, and still confirms the SDK tarball installs noprimitivebin (the CLI ships that bin from the separatecli-nodepackage).
Changing only one SDK's internal implementation, with no intended change to cross-language public behavior, does not require touching the shared fixture contract.
Tooling model#
The root Makefile is the primary task interface for every workflow; each SDK keeps its own native package tooling (pnpm, uv, go) inside its own directory:
make node-generate python-generate go-generate
make check
make build
make shared-check
Drop to a single SDK's native commands only when iterating locally on that one language before the commit that must include all three regenerated outputs:
cd sdk-node && pnpm typecheck && pnpm test
cd sdk-python && uv sync --dev && uv run pytest && uv run ruff check . && uv run basedpyright
cd sdk-go && go test ./... && go test -run TestSharedCompatibilityFixtures ./...
.github/workflows/sdk-checks.yml runs the same root make targets contributors use locally, across five categories: Node SDK, Node CLI, Python SDK, Go SDK, and shared fixture compatibility across all three languages. Using the same targets in CI and locally keeps the documented workflow and the enforced workflow aligned.
Release process#
Each package versions and releases independently, triggered by a version bump landing on main:
| Package | Version file | Release trigger |
|---|---|---|
Node SDK (@primitivedotdev/sdk) | sdk-node/package.json | PR merges with a new version |
CLI (primitive, mirrored as primcli / @primitivedotdev/cli) | cli-node/package.json | PR merges with a new version |
Python SDK (primitivedotdev) | sdk-python/pyproject.toml | PR merges with a new version |
Go SDK (github.com/primitivedotdev/sdks/sdk-go) | sdk-go/VERSION | PR merges with a new value (starts at unreleased, so the first automation PR does not tag a release) |
Before releasing any package#
- Confirm the working tree is clean.
- Update the relevant version metadata in a release PR.
- If the webhook or API contract changed, regenerate artifacts for every affected SDK.
- Ensure the release PR passes the required
SDK Checksworkflow. - Review the SDK README and changelog notes for public API changes.
- Merge the release PR into
main.
- 1
Node SDK release#
Open a PR bumping
sdk-node/package.jsonto the target version and merge it. TheNode Releaseworkflow verifies the version bump, publishes to npm via trusted publishing/OIDC, and tagssdk-node/vX.Y.Zplus a GitHub release. Verify withnpm view @primitivedotdev/sdk version, and confirm the packed tarball exposes the root,webhook,api,openapi,contract, andparsersubpaths, and installs noprimitivebin. - 2
CLI release#
Open a PR bumping
cli-node/package.jsonand merge it. TheCLI Releaseworkflow verifies the bump, publishes to npm, and tagscli-node/vX.Y.Z. The same workflow mirror-publishes two identical builds underprimcliand the legacy scoped@primitivedotdev/cli, locked to the same version; both are no-ops when that version already exists, so a re-run is safe. Verify withnpm view primitive version,npm view primcli version, andnpm view @primitivedotdev/cli version. - 3
Python SDK release#
Open a PR bumping
sdk-python/pyproject.tomland merge it. ThePython Releaseworkflow verifies the bump, publishes to PyPI, and tagssdk-python/vX.Y.Z. Verify the release on PyPI. - 4
Go SDK release#
Open a PR updating
sdk-go/VERSIONto a semantic version (e.g.0.1.0) and merge it. TheGo Releaseworkflow creates the subdirectory-prefixedsdk-go/vX.Y.Ztag plus a GitHub release. Verify the tag resolves through the Go module proxy.
Coordinate Node SDK and CLI releases when both ship in the same cycle: cut the SDK release first so its npm version is published, then bump the CLI's @primitivedotdev/sdk dependency range if needed before shipping the CLI.
Shared contract changes in a release#
If a release includes schema or shared-fixture changes:
- Update
json-schema/email-received-event.schema.json. - Regenerate SDK artifacts.
- Update
test-fixtures/if the behavioral contract changed. - Ensure the PR passes
SDK Checksagain before merging.
If a release includes API spec changes:
- Update
openapi/primitive-api.yaml. - Regenerate the Node, Python, and Go API clients.
- Ensure the PR passes
SDK Checksagain before merging.
Both npm packages (@primitivedotdev/sdk and primitive, plus its mirrors) use npm trusted publishing from GitHub Actions; no npm API tokens are configured. A brand-new mirror package name needs a one-time manual npm publish to claim it before trusted publishing can take over.
Next steps#
See how the OpenAPI source of truth flows into per-language generators and why api-core is never published.
Regenerating SDK Code from the OpenAPI SpecRun the end-to-end codegen pipeline across all three SDK languages in one pass.
OpenAPI Spec Normalization and Codegen ArtifactsLearn how the 3.1 YAML spec becomes the codegen-friendly 3.0.3 JSON artifact.
Webhook Events OverviewUnderstand the shared webhook contract and forward-compatibility guarantees in full.
Was this page helpful?