Seagull is a contract-first OpenAPI SDK, docs, and publishing pipeline - driven by a single config file. Point it at your `openapi.yaml` files, tell it which SDK artifacts you want and it lints, bundles, generates, documents, and publishes them.
Built to manage several services' contracts from one place - each service just needs an entry in the config; each artifact is generated by a reusable, shareable "recipe".
| Stage | What it does |
|---|---|
| Lint | Validates every contract's OpenAPI spec for structural and style issues |
| Bundle | Dereferences and bundles each spec into dist/specs |
| Generate | Runs each configured "recipe" through openapi-generator-cli or openapi-typescript |
| Document | Builds an interactive docs site across every contract into dist/docs |
| Publish | Pushes generated SDKs to orphan git branches/tags, and to artifact registries |
flowchart LR
A["Contract"] --> B["Lint"]
B --> C["Bundle<br/>(spec files)"]
C --> D["Generate<br/>(SDK artifacts)"]
C --> E["Docs website"]
D --> F["Publish SDK<br/>(git branch + tag)"]
D --> G["Publish registries<br/>(npm / Maven)"]
classDef stage fill:#363636,stroke:#666,color:#fff,rx:6,ry:6
class A,B,C,D,E,F,G stage
This repo is a monorepo of four packages, all versioned and published together
(see .changeset/config.json's fixed group and
RELEASING.md). Most people only need the first one - it bundles
the other three straight into its own dist/ via tsdown.
The three internal packages are published independently too, for anyone who
wants a smaller dependency (e.g. scripting against just
@octalmesh/seagull-core's config loader without pulling in the CLI or the docs
generator).
| Package | Role |
|---|---|
@octalmesh/seagull |
The seagull CLI + programmatic API. What almost everyone should install. |
@octalmesh/seagull-core |
Config loading, the Generator primitive, built-in generators, version/publishing logic, etc. |
@octalmesh/seagull-cli |
Pipeline commands (lint/bundle/generate/publish/...) + the commander program. |
@octalmesh/seagull-docs |
Docs-site generation from bundled specs, plus a local dev server for previewing the site. |
npm install -D @octalmesh/seagullUnder the hood, generate shells out to @openapitools/openapi-generator-cli
(needs a JVM on PATH) and openapi-typescript; lint/bundle and docs
each rely on their own established OpenAPI tooling under the hood. All of these
are @octalmesh/seagull's own dependencies, resolved via Node's module
resolution.
Create a config file at the root of your contracts repo (any of
.seagull, .seagull.yaml, .seagull.yml, seagull.yaml, seagull.yml):
# Seagull config version
configVersion: 1
# Custom variables to use in config
vars:
org: "your-npm-scope"
repository:
owner: "your-org"
repo: "your-contracts-repo"
# Documentation configuration
docs:
server:
host: "localhost"
port: 8080
metadata:
title: "Your API Docs"
description: "Generated API documentation"
favicon: "/favicon.ico"
baseServerUrl: "https://api.example.com"
# Publishing configuration
publishing:
branch: "sdk/svc-{service}/{id}"
tag: "svc-{service}-{id}-v{version}"
repositoryUrl: "https://github.com/{vars.repository.owner}/{vars.repository.repo}"
npm:
registry: "https://registry.npmjs.org"
access: "public"
maven:
repositoryId: "github"
repositoryUrl: "https://maven.pkg.github.com/{vars.repository.owner}/{vars.repository.repo}"
# Generators configuration used to produce SDK artifacts
generators:
ts-client:
tool: "openapi-generator"
generator: "typescript-fetch"
lang: "typescript"
kind: "client"
package: "@{vars.org}/{service}-client"
# Per-service contract configuration
contracts:
- name: "auth"
title: "Auth Service API"
entrypoint: "specs/auth/openapi.yaml"
artifacts:
- "ts-client"Then:
npx seagull lint
npx seagull bundle
npx seagull generate
npx seagull docs generate && npx seagull docs serveSee examples/ for complete, runnable configs covering
every generator/publishing/docs feature, plus an API walkthrough.
-
configVersion:(required) - which version of the config schema this file targets. Decoupled from@octalmesh/seagull's own npm version on purpose: this only changes ifseagull.yaml's shape changes in a breaking way, so an old config fails with a clear "expected configVersion 1" error. -
generators:- reusable recipes: atool(openapi-generatororopenapi-typescript), which-gtemplate to use, and naming templates for the npm package / Go module / Maven coordinates. Any string field may reference{vars.some.nested.key}or{service}(the current contract'sname). -
contracts:- one entry per service (name,title,entrypoint, and whichgenerators:it wants underartifacts:, by id). Two services don't need the same generators - a contract can reference a generator with a per-contract override instead of duplicating the whole recipe:contracts: - name: "payment" artifacts: - generator: "java-client" as: "java-client-legacy" # renames this artifact's output folder/branch/tag overrides: generator: "java-legacy-template" additionalProperties: library: "jersey2"
-
vars:- a free-form tree for anything used in naming templates. Nest however deep is useful; every leaf is addressable as{vars.a.b.c}. -
paths:- onlydist:is required;specs/docs/sdkdefault to<dist>/specs,<dist>/docs,<dist>/sdkand only need to be set to override that.specFormat:controls what format(s)seagull bundlewrites specs in -json(default),yaml, or a list of both (specFormat: [json, yaml]) to bundle into more than one format at once; Redocly infers the output format from the file extension on its own, so this is a free choice, not a compatibility trade-off. When more than one format is configured, the first one listed is the "primary" format SDK generation and the docs site actually read from - the rest are bundled as additional static artifacts alongside it. -
docs:-server: { host, port }fordocs serve, andmetadata: { title, description, favicon, baseServerUrl }for the generated docs site. -
publishing:(required) - see below.
A typo or missing field fails immediately with a readable, path-annotated error. Config is validated on every run.
seagull.yaml (and redocly.base.yaml) also support the YAML <<: *anchor
merge key, so shared fields don't need to be repeated across every entry:
_defaults: &defaults
lang: "typescript"
generators:
ts-server:
<<: *defaults
tool: "openapi-typescript"
kind: "server"
package: "@{vars.org}/{service}-server"
ts-client:
<<: *defaults
tool: "openapi-generator"
generator: "typescript-fetch"
kind: "client"
package: "@{vars.org}/{service}-client"publishing: controls where things get published to - git branch/tag naming,
and npm/Maven registry URLs. It's required at the root level: seagull has no
built-in convention here, so a config that omits it fails validation with a
message pointing at exactly what's missing, rather than silently applying an
opinionated default nobody chose.
publishing:
branch: "sdk/svc-{service}/{id}" # git branch each artifact publishes to
tag: "svc-{service}-{id}-v{version}" # git tag - the only field where {version} is available
repositoryUrl: "https://github.com/{vars.repository.owner}/{vars.repository.repo}" # git remote URL for pushing branches/tags
npm:
registry: "https://npm.pkg.github.com"
access: "restricted" # or "public"
maven:
repositoryId: "github"
repositoryUrl: "https://maven.pkg.github.com/{vars.repository.owner}/{vars.repository.repo}"Every field is a template - the same {...} engine as naming templates, plus
{id} (the artifact's id) and, for tag only, {version} (resolved once the
contract's spec is bundled, since a branch is created before a version is known
but a tag isn't).
Like additionalProperties and readme, publishing: can be overridden
per-generator (generators.<id>.publishing) or per-contract-artifact
(artifacts[].overrides.publishing) - only the fields that differ need
repeating, the rest fall through to the root-level config:
flowchart TD
A["Root config<br/>(fallback for everything)"] --> B["Generator override<br/>(per recipe)"]
B --> C["Artifact override<br/>(per contract)"]
C --> D(["Effective config<br/>for this artifact"])
classDef layer fill:#363636,stroke:#666,color:#fff,rx:6,ry:6
classDef result fill:#1f6feb,stroke:#1f6feb,color:#fff,rx:20,ry:20
class A,B,C layer
class D result
The same three-layer precedence (root -> generator -> artifact overrides)
applies to additionalProperties and readme too, not just publishing.
generators:
ts-client:
# ...
publishing:
npm:
registry: "https://registry.internal.example.com" # every contract's ts-client uses this registry
contracts:
- name: "payment"
artifacts:
- generator: "ts-client"
overrides:
publishing:
branch: "custom/{service}-{id}-branch" # ...except payment's ts-client, which also uses a different branchEvery generated artifact gets a README.md - by default a sensible built-in
template for its language/kind. To use your own, point readme: at a template
file (path relative to the config file):
generators:
ts-client:
# ...
readme: "readme-templates/ts-client.md"Template files support the same {...} placeholders as naming templates, plus a
few more:
| Placeholder | Value |
|---|---|
{service} |
The contract's name |
{title} |
The contract's title |
{version} |
The resolved SDK version |
{vars.*} |
Anything under vars: |
{artifact.id} |
The artifact's id (as listed under artifacts:) |
{artifact.package} |
Resolved npm package name (TypeScript) |
{artifact.goModule} / {artifact.goPackageName} |
Resolved Go naming |
{artifact.maven.groupId} / {artifact.maven.artifactId} |
Resolved Maven coordinates |
{artifact.branch} / {artifact.tag} |
Resolved publishing branch / tag |
{artifact.npmRegistry} / {artifact.mavenRepositoryUrl} |
Resolved registry URLs from publishing: |
An unresolvable placeholder fails the build loudly (a typo'd {vesion} won't
silently ship as literal text).
seagull lint Lint every contract's OpenAPI spec
seagull bundle Bundle every contract's spec into dist/specs
seagull generate Generate every configured SDK artifact into dist/sdk
seagull clean Remove the dist directory
seagull docs generate Generate the documentation site into dist/docs
seagull docs serve Serve the generated documentation site locally
seagull publish sdk [--dry-run] Publish generated SDKs to their git branches/tags
seagull publish registries [--dry-run] npm publish / mvn deploy the registry-backed artifacts
Every command accepts -c, --config <path> to point at a config file outside
the current directory.
pnpm install # install dependencies for all packages
pnpm run build # builds packages/* first (topological), then bundles the root package
pnpm run typecheck # run after build - resolves the workspace packages via their built dist/
pnpm run lint # lint all packages
pnpm run test # run all testsSee CONTRIBUTING.md for how to propose changes, and RELEASING.md for how versioning and publishing work.
- Feel free to use this project for any purpose, including commercial applications.
- You are permitted to modify, distribute, and include this project in any form, as long as the original copyright notice is retained.
- If you share or publish modified versions, attribution to the original GitHub repository is appreciated.
- This software is provided "as is", without any warranties or guarantees, as detailed in the license terms.
- Feel free to use this project for any purpose, including commercial applications.
- You are permitted to modify, distribute, and include this project in any form, as long as the original copyright notice is retained.
- If you share or publish modified versions, attribution to the original GitHub repository is appreciated.
- This software is provided "as is", without any warranties or guarantees, as detailed in the license terms.