Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started

Multi-VCS release flow

Glotto is provider-agnostic from day one. A single VcsProvider abstraction lets it push regenerated SDKs and open release PRs across GitHub, GitLab, and Bitbucket — including self-managed instances via host config and PAT fallbacks. Gitea and generic-git are on the roadmap. Self-hosted GitLab, poorly served by GitHub-app-first competitors, is a first-class target.

How a release happens

  1. A spec change is pushed; the provider’s webhook is ingested and normalized into a provider-agnostic GlottoVcsEvent — provider-specific shapes never leak past the normalizer.
  2. The release-PR orchestrator detects the spec diff, regenerates via the pipeline, and opens a PR with a changelog against the SDK target repo.
  3. The per-provider drift-check workflow keeps committed output honest on every PR.

The orchestration layer programs against VcsProvider + GlottoVcsEvent only, so adding a provider doesn’t touch the release logic.

Three branches: generatednextmain

Every SDK repository Glotto manages holds the same three branches, and each one answers a different question:

Branch Holds Written by
generated the pristine generator output for the current spec — never merged, never hand-edited every regeneration
next generated three-way-merged with your custom code, collecting changes for the next release every regeneration
main released code merging the release PR

generated is what makes custom code survive regeneration: it is the merge base, so Glotto can tell your edits apart from its own previous output instead of guessing. That is why nothing ever lands on it but a fresh generation — see custom code for the merge itself.

The release PR is always nextmain. Merging it is the release.

You can rename any of the three per target with targets.<lang>.release, which is also the migration path for a repository already on other branch names — a config line, not a migration. When a target sits in a subdirectory of a shared repository (repo_path), generated and next each take a per-language suffix so two SDKs in one repository never share a branch.

Two repositories: staging and production

Every SDK target has two repositories, and they answer different questions.

The staging repository is hosted by Glotto, one per project per language, named <hosted-org>/<org>-<project>-<lang>. It is created with your project, and every build lands there first: the pristine output on generated, with next and main fast-forwarded to the same revision. It carries no custom code, ever — which is what makes it the honest answer to “what did Glotto generate from this spec?”.

You can install from it directly, with your own forge credentials:

npm i github:<hosted-org>/<org>-<project>-<lang>

Two things to know before you run that. The staging repository is created private, so the command needs a credential with read access to it — an authenticated remote, not an anonymous clone. And the tree it installs is the pristine SDK: any custom code you keep in your production repository is not in it, by design.

A hand-edit pushed to a staging branch stops the next build rather than being merged or overwritten. Glotto fast-forwards next and main and never forces a ref, so a branch whose tip is not on the build line makes the next build fail with staging_diverged, naming the branch. Recovery is moving that branch back onto the build line — a force-push by someone with write access on the hosted repository. Glotto does not do it for you: a staging repository that accepted hand-edits would be a second production repository, and “which one is right?” would return.

The production repository is yours (targets.<lang>.repo). It is where the full release flow above runs — generated, the three-way merge into next, the release PR nextmain. Selecting it needs an organization-level connection for its provider first: connect your VCS, then pick the repository. Glotto reads the repository’s default branch when you select it and records it as targets.<lang>.release.base_branch when it is not main, so a trunk repository integrates on trunk without any further configuration.

Staging is GitHub-only today. GitLab, Bitbucket and Azure Repos remain fully supported as production repositories; hosted staging repositories on those forges are not offered yet.

What a build reports

Each build of each target records one row, and its status says exactly how far it got:

Status Means
building the build has started; nothing has been pushed yet
build_failed generation did not complete (or your plan is out of builds) — nothing was pushed anywhere
staging_failed the build could not be shown on staging, so it was not proposed to your repository
staged staging holds the build; there is no production repository yet, or the release is set to manual
production_failed staging holds the build and is intact; the release on your own repository did not complete
released the release PR on your repository was opened or refreshed

What the orchestrator opens

Nothing below is typed by hand: both panes are what runReleaseFlow — the same orchestrator step 2 describes — decided when a spec was pushed. It is running here against the in-memory VcsProvider the release suite drives, so what you are reading is the release Glotto decided on, not a transcript of one forge’s API. Which forge it lands on is the part the abstraction makes uninteresting, and the per-provider suites are what hold that.

a spec pushed to `main` for the first time — the orchestrator opens the release PR

A spec pushed to main for the first time. The regenerated SDK, on the next branch, as a PR against main.

  • files in the release 19
  • branch next
title   release: v0.1.0-alpha.1
rule    first release; alpha: no verification report
head    next
base    main
state   open

files   (19)
          README.md
          package.json
          sdk.test.ts
          src/client.ts
          src/core/client-core.ts
          src/core/model-fields.ts
          src/core/runtime.ts
          src/core/types.ts
          src/errors.ts
          src/index.ts
          src/lib/index.ts
          src/models/pet.ts
          src/models/store.ts
          src/models/vaccination.ts
          src/resources/pets-vaccinations.ts
          src/resources/pets.ts
          src/resources/stores.ts
          tsconfig.json
          tsconfig.test.json
regenerated + byte-diffed in CI runReleaseFlow (@glotto/core-vcs) examples/stainless-migration/inputs 3906fa591adb

Push a spec that gained one operation, and the same flow re-releases — with the SDK following:

the spec gains `GET /health` — the same flow re-releases, and the SDK follows

The spec gained GET /health. The client and its README moved; nothing else did.

  • files in the release 20
  • branch next
title   release: v0.1.0-alpha.1
rule    from the release PR title
head    next
base    main
state   open

files   (20)
          README.md   ← changed
          package.json
          sdk.test.ts   ← changed
          src/client.ts   ← changed
          src/core/client-core.ts
          src/core/model-fields.ts
          src/core/runtime.ts
          src/core/types.ts
          src/errors.ts
          src/index.ts   ← changed
          src/lib/index.ts
          src/models/pet.ts
          src/models/store.ts
          src/models/vaccination.ts
          src/resources/health.ts   ← changed
          src/resources/pets-vaccinations.ts
          src/resources/pets.ts
          src/resources/stores.ts
          tsconfig.json
          tsconfig.test.json
regenerated + byte-diffed in CI runReleaseFlow (@glotto/core-vcs) examples/stainless-migration/inputs c7245baf0840

Where the version number comes from

The release PR’s version is derived, not chosen, and the PR body’s Version: line names the rule that produced it — so a reader never has to go and find out why a number is what it is. Four inputs, read in this order:

  1. The PR title wins. A release PR is titled release: vX.Y.Z, and a version written there is honored verbatim — nothing below is consulted. It is the one place a human sets 1.0.0 or a channel Glotto does not know about.
  2. The bump comes from the contract diff. An operation or model removed or changed is a major bump (a minor one below 1.0.0); something added is a minor bump; no API change is a patch. Below 1.0.0 the derivation never crosses to 1.0.00.9.0 with a removed operation becomes 0.10.0 — because that line is a product decision, not an arithmetic one.
  3. The prerelease channel comes from your latest verification report. A failed compile or contract check — or a hand-edited managed file — is -alpha.N. Checks that have not run are -beta.N. Only a report where everything passed produces a plain X.Y.Z.
  4. No report is -alpha.N, not a plain version. A bare version is a claim that the build was verified, and “we could not find the report” is not that claim. A project that has not set up check ingestion therefore sees -beta.N once its reports arrive and -alpha.N until then; a diff Glotto could not compute is a patch bump whose rule line says so, never “no API change”.

Two consequences worth knowing:

  • An open release PR keeps the version it was opened with. The channel is decided when the PR opens and frozen for its lifetime, exactly as the patch number already is — because rule 1 reads the title the PR already carries. Once your checks pass, edit the title to the version you want (or close the PR so the next build opens a fresh one).
  • A prerelease never becomes the default install. An -alpha.N / -beta.N npm publish goes out under that channel’s dist-tag (alpha, beta, or npm’s next for any other identifier), never latest. pip, RubyGems, NuGet, Hex and pub already treat a prerelease suffix as opt-in.

Review ownership — one block, four forges

The same abstraction covers who reviews the SDK repos Glotto manages. Declare it once in code_owners and each target’s forge receives it the way that forge implements review ownership — which is not the same way in any two of them:

Provider How ownership arrives Path scoping
GitHub .github/CODEOWNERS, emitted by glotto generate Per rule
GitLab .gitlab/CODEOWNERS, emitted by glotto generate Per rule
Bitbucket Repository default reviewers, via code-owner application None — repo-wide
Azure Repos A required-reviewers branch policy, via code-owner application Per rule

The honest deliverable is review ownership per provider, not “a CODEOWNERS file”, and the part that matters is what happens where the mapping is imperfect. Bitbucket’s default reviewers cannot be scoped to a path, so code-owner application tells you which of your patterns it had to flatten instead of reporting a plain success. An owner no workspace member matches comes back named, with the reason, while the owners that did resolve still apply. And a target whose SDK lives under a repo_path subtree gets no file at all — GitHub and GitLab read CODEOWNERS only from the repository root — which glotto generate and glotto generate both say out loud rather than emitting a file nothing will ever open.

That last property is the point. A governance feature that quietly does nothing on half your repositories is worse than one you know you have to configure by hand.

Preview builds — try the SDK before you merge

The same normalized webhook powers preview builds: when a pull request is opened on your spec repo, Glotto regenerates every SDK target and posts one comment on the PR carrying, per target, its build status, a file-level diff against the last released build, and a working install command.

Target How you install a preview
TypeScript, React Native npm install '<url>'
Python pip install '<url>'
The other ten languages Download the archive, then use a local-path dependency (command below)

For those languages, run curl -L '<url>' | tar xz, then point the project at the unpacked local path.

Install links are unguessable and expire after 14 days — treat one as a secret, since anyone holding it can install that build. Previews are opt-in per project (preview.enabled) and off by default.

This is the guarantee made tangible: rather than trusting that regeneration will do the right thing at release, you hold the regenerated artifact while the change is still reviewable.

When your spec lives at a URL

The flow above starts with a push. If your OpenAPI document is published at a URL rather than committed to a repository Glotto is connected to, there is no push to react to — so Glotto can watch the URL instead.

Point openapi.source at your published document, as you already would:

openapi:
  source: https://api.example.com/openapi.yaml

Then enable polling for the project: set auto_poll on the project’s verification source (with poll_interval_minutes if you want a cadence other than the hourly default). On its cadence Glotto reads your repository’s own glotto.yml, resolves the URL that file names, and fetches it. When the document has changed, it records the revision and runs exactly the release above — the same regeneration, the same three-way merge with your custom code, the same release PR. Push-triggered and poll-triggered releases converge on one open release PR, so running both is the intended configuration rather than a conflict.

Four things worth knowing before you turn it on:

  • It is opt-in per project and off by default. A poll that finds a change regenerates, and regeneration counts against your plan — so no existing project starts doing this because the feature shipped.
  • An unchanged spec costs nothing. Each poll sends a conditional request; an origin that answers “not modified”, or that serves bytes identical to the last revision, produces no regeneration and consumes no quota. The default cadence is hourly, configurable per project.
  • The address stays in your repository. Glotto never stores a copy of your spec URL — it reads openapi.source from your glotto.yml every time. Changing where your spec lives is a commit you review, not a setting in someone else’s database, and it means a local glotto generate and a hosted regeneration can never disagree about which document they used.
  • The document has to be reachable without credentials. openapi.source takes a URL, not a URL plus headers, so there is nowhere to put an API key — a spec behind an Authorization header cannot be polled. This is the same limit a local glotto generate against that URL already has, not one polling adds. If your spec is private, commit it to a connected repository and use the push flow above.
  • Breaking-change detection still needs a committed baseline. A URL serves one current document, so there is no older per-branch version to diff against — unlike a spec in git, where the previous commit is the baseline. The breaking-change check covers this case by diffing against a baseline document you commit. Polling keeps your SDKs current; it does not by itself give a URL source the history a repository has.