---
title: "Monorepo Structure and Release Process"
canonical: "https://test.abhinandan.one/monorepo-and-releases"
markdown_url: "https://test.abhinandan.one/monorepo-and-releases.md"
publisher: "Primitive SDKs"
kind: "concept"
content_type: "reference"
category: "Core Concepts"
description: "Automated releases fire when a PR merges a version bump in sdk-node/package.json, cli-node/package.json, sdk-python/pyproject.toml, or sdk-go/VERSION."
keywords: ["sdks monorepo layout", "openapi/primitive-api.yaml", "json-schema/email-received-event.schema.json", "make node-generate python-generate go-generate", "make check", "sdk-go/VERSION release"]
last_modified: "2026-08-11T18:55:07.416344+00:00"
published_at: "2026-08-11T18:55:07.207702+00:00"
source_files:
  - "docs/architecture.md"
  - "RELEASE.md"
sections:
  - {anchor: "repository-layout", title: "Repository layout"}
  - {anchor: "the-two-shared-contracts", title: "The two shared contracts"}
  - {anchor: "sdk-layers", title: "SDK layers"}
  - {anchor: "change-strategy", title: "Change strategy"}
  - {anchor: "tooling-model", title: "Tooling model"}
  - {anchor: "release-process", title: "Release process"}
  - {anchor: "before-releasing-any-package", title: "Before releasing any package"}
  - {anchor: "step-node-sdk-release", title: "Node SDK release"}
  - {anchor: "step-cli-release", title: "CLI release"}
  - {anchor: "step-python-sdk-release", title: "Python SDK release"}
  - {anchor: "step-go-sdk-release", title: "Go SDK release"}
  - {anchor: "shared-contract-changes-in-a-release", title: "Shared contract changes in a release"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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:

```text
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 (the `email.received` / `EmailReceivedEvent` shape).
- **`sdk-node/`**, **`sdk-python/`**, **`sdk-go/`**: the three language SDKs.
- **`cli-node/`**: the `primitive` CLI, built on the Node SDK and `packages/api-core`.
- **`packages/api-core/`**: the workspace-internal, never-published TypeScript package holding the generated OpenAPI client, the operation manifest, and `PrimitiveApiClient`. See [What is API Core?](https://test.abhinandan.one/api-core-overview.md).
- **`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:

1. verifies the `Primitive-Signature` header
2. parses the raw request body
3. validates the payload against the canonical schema
4. exposes a typed `email.received` event model
5. 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](https://test.abhinandan.one/webhook-events.md); the normalized `ReceivedEmail` object built on top of it is covered in [Inbound and Outbound Email Model](https://test.abhinandan.one/email-model.md).

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](https://test.abhinandan.one/codegen-architecture.md) and [OpenAPI Spec Normalization and Codegen Artifacts](https://test.abhinandan.one/openapi-spec-normalization.md). Running the full regeneration pipeline end to end is covered in [Regenerating SDK Code from the OpenAPI Spec](https://test.abhinandan.one/codegen-workflow.md).

> **Tip:** 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:

1. Update `json-schema/email-received-event.schema.json` if the payload contract itself changes.
2. Regenerate language-specific artifacts for all three SDKs.
3. Update `test-fixtures/` when the expected cross-language behavior changes.
4. Run `make check`.
5. Review each SDK for language-specific helper implications.

When changing the HTTP API contract:

1. Update `openapi/primitive-api.yaml`.
2. Regenerate the Node, Python, and Go API clients.
3. Run `make node-check python-check go-check`.
4. Verify the Node smoke test still exposes `api` and `openapi` subpaths, and still confirms the SDK tarball installs no `primitive` bin (the CLI ships that bin from the separate `cli-node` package).

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:

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

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

1. Confirm the working tree is clean.
2. Update the relevant version metadata in a release PR.
3. If the webhook or API contract changed, regenerate artifacts for every affected SDK.
4. Ensure the release PR passes the required `SDK Checks` workflow.
5. Review the SDK README and changelog notes for public API changes.
6. Merge the release PR into `main`.

#### 1. Node SDK release

Open a PR bumping `sdk-node/package.json` to the target version and merge it. The `Node Release` workflow verifies the version bump, publishes to npm via trusted publishing/OIDC, and tags `sdk-node/vX.Y.Z` plus a GitHub release. Verify with `npm view @primitivedotdev/sdk version`, and confirm the packed tarball exposes the root, `webhook`, `api`, `openapi`, `contract`, and `parser` subpaths, and installs **no** `primitive` bin.

#### 2. CLI release

Open a PR bumping `cli-node/package.json` and merge it. The `CLI Release` workflow verifies the bump, publishes to npm, and tags `cli-node/vX.Y.Z`. The same workflow mirror-publishes two identical builds under `primcli` and 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 with `npm view primitive version`, `npm view primcli version`, and `npm view @primitivedotdev/cli version`.

#### 3. Python SDK release

Open a PR bumping `sdk-python/pyproject.toml` and merge it. The `Python Release` workflow verifies the bump, publishes to PyPI, and tags `sdk-python/vX.Y.Z`. Verify the release on PyPI.

#### 4. Go SDK release

Open a PR updating `sdk-go/VERSION` to a semantic version (e.g. `0.1.0`) and merge it. The `Go Release` workflow creates the subdirectory-prefixed `sdk-go/vX.Y.Z` tag plus a GitHub release. Verify the tag resolves through the Go module proxy.

> **Note:** 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:

1. Update `json-schema/email-received-event.schema.json`.
2. Regenerate SDK artifacts.
3. Update `test-fixtures/` if the behavioral contract changed.
4. Ensure the PR passes `SDK Checks` again before merging.

If a release includes API spec changes:

1. Update `openapi/primitive-api.yaml`.
2. Regenerate the Node, Python, and Go API clients.
3. Ensure the PR passes `SDK Checks` again 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.
