The Stellar Index ships two version dimensions, each governed by SemVer (vX.Y.Z) but with independent tag namespaces and independent semantics.
| Surface | Tag form | Bump rules |
|---|---|---|
**pkg/* Go modules** (e.g. pkg/client) | pkg/<name>/vX.Y.Z | Standard Go-module SemVer (API surface) |
Binary releases (stellarindex-api, stellarindex-indexer, …) | vX.Y.Z (root tag) | Operator-impact SemVer (config / wire / behaviour) |
The two clocks tick independently. A binary release v0.4.0 may contain pkg/client v0.2.1 while bundling unchanged versions of any other pkg/* modules. The CHANGELOG.md entry for that release lists the new pkg/* versions it contains.
SemVer rules for pkg/*
What's covered
Every package under pkg/ is part of the public API surface and is bound by the rules below. **internal/* is NOT** — internal packages can be refactored, renamed, or deleted in any PR.
Currently shipped:
pkg/client— Go SDK for the public API
(#201). Wire-shape types (Envelope, Flags, Pagination, AssetDetail, …) live in pkg/client/types.go rather than a separate pkg/types package — see CLAUDE.md "Repo map" for the rationale. The server's internal/api/v1 defines its own envelope intentionally; the duplication is the SemVer firewall between the SDK's public surface and internal handler shapes.
What constitutes a breaking change
Any of the following bumps the major version:
- Removing or renaming a public identifier (type, function, variable, constant, method)
- Removing a struct field, method receiver, or interface method
- Changing a function/method signature in a non-additive way (changing parameter types, return types, or order)
- Adding a method to an interface (existing implementers stop satisfying the interface)
- Changing the JSON wire shape produced by a public type's
MarshalJSON(or its generated default) - Tightening input validation in a way that rejects previously-accepted inputs
- Changing the documented error semantics — e.g. a function that previously returned
nil, ErrNotFoundnow returnsnil, nil
Any of the following bumps the minor version:
- Adding a new exported identifier
- Adding a new field to a struct (with a sensible zero value)
- Loosening input validation
- Adding a new optional configuration field
- Adding a new error sentinel that's a *more specific* version of an existing one (callers using
errors.Isagainst the existing sentinel still match the new one)
Any of the following is patch-only:
- Bug fixes that preserve documented behaviour
- Performance improvements with no API change
- Documentation-only changes
- Test-only changes
- Internal refactoring with no
pkg/*impact
Pre-v1.0 (v0.x) policy
pkg/client is currently v0.1.0. Until we tag v1.0.0:
- Breaking changes are allowed but MUST be called out in
CHANGELOG.mdunder the version where they land - Each breaking change should bump the *minor* version (
v0.1 → v0.2), not the major — Go modules treatv0.xas inherently unstable per the spec - Public-facing release notes flag every breaking change loudly
When we tag v1.0.0 (target: end of public-launch week), the contract becomes binding — breaking changes after that require a new major version (v2.0.0).
Deprecation policy
When a pkg/* identifier is destined for removal:
- Mark the identifier with a
// Deprecated: <reason>. Use <replacement>.godoc comment in the same release - Keep it in place for at least one minor version
- Remove only at the next major version boundary
- CHANGELOG entry under the deprecating release calls it out; release notes for the removing release reiterate it
Example:
// Deprecated: use Client.PriceTip instead. Removed in v2.0.0.
func (c *Client) PriceLive(ctx context.Context, asset string) (*Envelope[PriceSnapshot], error) {
return c.PriceTip(ctx, PriceQuery{Asset: asset})
}Tagging mechanics
Go modules take version info from git tags of the form pkg/<name>/v<major>.<minor>.<patch>:
# Bump pkg/client to v0.2.0
git tag pkg/client/v0.2.0
git push origin pkg/client/v0.2.0Pre-tag manual checks (the release runbook §"Pre-flight" captures the same set for the binary clock):
- Working tree matches
mainand the tagged commit (git status
is clean; git log -1 is the commit you intend to tag).
CHANGELOG.mdhas an entry under the new version with the PRs
it includes.
- The package's own version constant (if any) matches the tag.
make testis green at the tagged commit.
SemVer rules for binary releases
Format
vX.Y.Z:
- `X` (major) — bumped when an operator MUST take action beyond the standard restart to upgrade (config schema break, removed endpoint, removed CLI flag, manual data backfill required, breaking wire-shape change)
- `Y` (minor) — bumped on additive changes that need no operator action (new endpoint, new optional config field, new source connector, new aggregation behaviour with safe defaults)
- `Z` (patch) — bumped on operator-invisible changes (bug fixes, performance, internal refactoring, doc-only)
Examples:
v0.1.0— initial public releasev0.2.0— adds new SSE endpoint (additive)v0.2.1— patch fix for an aggregator off-by-onev1.0.0— first stable cut, contract becomes binding
Pre-v1.0 (v0.x) policy
Until we tag v1.0.0:
- Breaking changes bump the minor version (
v0.1.x → v0.2.0), matching thepkg/*pre-v1 convention. Major bump is reserved for the v1.0 cut. - The CHANGELOG entry under the breaking version MUST call out the operator action explicitly (config edit, migration, etc.).
- Release notes lead with the breaking change in the summary paragraph.
What constitutes a breaking change for binaries
Any of the following bumps minor (pre-v1) or major (post-v1):
- Config schema break — a field in
/etc/stellarindex.tomlis removed, renamed, or its default semantics change in a way that affects existing operator configs - API wire-shape change — JSON response shape changes for an existing endpoint (field removed, field renamed, type changed)
- API endpoint removal or rename — operators with hardcoded URLs break
- CLI flag removal — operators with hardcoded systemd unit
ExecStart=lines break - DB migration that requires manual backfill —
stellarindex-migrate upis not sufficient; operator must run a separate one-off SQL/script - Source-connector removal — an enabled source goes away; operators relying on its data must reconfigure
- Behaviour change in fallback semantics — VWAP→TWAP→last-trade fallback chain behaves differently in a way operators must learn
Any of the following bumps the minor version (additive):
- New API endpoint
- New CLI flag (with safe default if omitted)
- New
/etc/stellarindex.tomlfield (with safe default if omitted) - New source connector (
enabled = falseby default — see[external]block convention) - New aggregation feature behind an opt-in flag
- New migration that runs forward-only via
stellarindex-migrate up - New observability metric
Any of the following is patch-only:
- Bug fixes that preserve documented behaviour
- Performance improvements with no operator-visible change
- Internal refactoring (
internal/*churn) - Documentation-only changes
- Test-only changes
- Dependency bumps that don't change behaviour
- Re-deploy of identical functionality (e.g. rebuild from same code with newer Go toolchain)
Tagging
Single repo-level tag at the commit you want to release:
git tag v0.2.0
git push origin v0.2.0The release builds every binary at this commit. stellarindex-api --version and stellarindex-indexer --version both report v0.2.0 for that release. The Makefile's git describe --tags --always --dirty populates internal/version.Version at build time via -ldflags.
What goes in a binary release note
Every release note (under ## [<version>] in CHANGELOG.md) MUST include:
- Stellar protocol version the release was tested against (e.g.
Tested against pubnet protocol 23) - **
pkg/*versions** included (e.g.Includes pkg/client v0.4.2) - Migration notes for any change that affects operators (config schema additions, DB migrations, runbook changes). If none, write "None."
- The standard Added/Changed/Deprecated/Removed/Fixed/Security sections
- Operator action required: yes/no on the first line — operators reading at-a-glance need to know whether the upgrade is "restart and done" or "edit config first"
Why SemVer (not CalVer) for binaries
We considered CalVer (YYYY.MM.DD.N) and switched to SemVer for the binary clock to match the pkg/* clock and to give operators a single mental model: "is this a `vX.0.0` cut? must I edit my config?" is more useful than "is this newer than what I'm running?" when releases land 2-3× per week.
The release-process runbook still records every cut's UTC date in the CHANGELOG section header so the calendar dimension is preserved in human-readable form (## [v0.2.0] — 2026-07-15).
Stability tiers within internal/*
internal/* is not version-controlled in the SemVer sense, but some packages are more refactor-safe than others:
| Package | Stability | Refactor cost |
|---|---|---|
internal/canonical | High — changes ripple through every source | Coordinated rename PR |
internal/api/v1 | High — wire-shape changes break clients | New endpoint instead of field-shape change |
internal/aggregate | Medium — internal consumers only | Standard PR review |
internal/sources/* | Low — per-source decoders churn frequently | Author + CODEOWNER review |
internal/divergence, internal/aggregate/anomaly, internal/aggregate/baseline, internal/aggregate/confidence, internal/aggregate/freeze, internal/archivecompleteness | Low — recent additions, expected to grow | Standard PR review |
This isn't a SemVer commitment — it's review-effort guidance. A PR touching internal/canonical.Trade's field set should land with explicit migration notes for every consumer; a PR adding a new source in internal/sources/<venue>/ is the normal flow.
Cross-references
- ADR-0005 — monorepo / one-Go-module decision; the SemVer commitment on
pkg/*lives here - `docs/operations/release-process.md` — runbook the release engineer follows; implements this policy
- `.github/RELEASE_NOTES_TEMPLATE.md` — fill-in template for GitHub Release notes
- `CHANGELOG.md` — every release's entry follows the rules above
- `pkg/client/doc.go` — package-level statement of v0.x stability promise