Skip to content

[Devops] Streamlined branching strategy for schema #490

Description

@lowlydba

♻️ This top comment is evergreen, and will evolve to match the current state of this effort.


Overview

Streamlined branching strategy and release automation for the schema repository. Replaces the old dev -> staging -> main manual flow and per-release long-lived branches (e.g. main_2026_02_12) with a two-branch model, automated CI safety nets, and PR-driven releases.

Live documentation: CONTRIBUTING.md (branch mechanics) and docs/versioning.md (version scheme + release process).

Two-branch model

  • main: integration branch. All normal work lands here. Always releasable.
  • vnext: accumulates major changes between releases. Continuously rebased onto main by CI. Merges to main at release time (regular merge, not squash).

Versioning model (settled in Phase 2.B)

  • Humans own the full <major>.<minor>.<patch> per package in pyproject.toml. Any bump merged to main cuts a per-package GitHub Release (tag <pkg>-v<version>) and, in Phase 3, a public PyPI publish.
  • Every PR that changes a package carries a towncrier changelog fragment, enforced by CI; release PRs fold fragments into CHANGELOG.md.
  • No-bump merges produce internal-only builds versioned <version>.postN+main.<sha> (or +vnext.<sha>) for CodeArtifact. >= consumers resolve them; public PyPI rejects them by construction.

Phases

Phase Title Status
Phase 0 Branch infrastructure ✅ Done
Phase 1 CI safety net ✅ Done
Phase 2.A Internal versions & version compute ✅ Done (#534)
Phase 2.B Release versions 🔄 PR open (#557)
Phase 3 Package publish workflows (main + PyPI releases) 🔄 PR open (#638)
Phase 4 vnext dev builds to CodeArtifact ⛔ Blocked on ops-team#299
V2 phase details (superseded by the phase issues, kept for history)

Phase 2, Versioning & release-trigger logic

2.A: Establishes per-package version scheme (<major>.<minor> git-controlled, <patch> CI-computed from CodeArtifact) and the compute-version composite action.

2.B: Implements release-event detection and the vnext->main release process. Requires decisions on changelog approach and patch destination (CA only vs PyPI).

Superseded: 2.B settled on human-owned full semver (patch included), towncrier fragments, per-package releases, and .postN internal builds. See #533 for the resolved decisions.

Phase 3

Implements build + publish automation. Destination depends on context: CA dev (vnext), CA (main patches), PyPI (major/minor bumps). Patch->PyPI decision is TBD and may affect this scope.

Superseded: see the current trigger x destination matrix in #509.

Phase 4

Originally an editorial pass on docs/branching-strategy.md + docs/versioning.md: diagrams, walkthroughs, FAQ, no new procedures.

Note: branch mechanics live in CONTRIBUTING.md; there is no docs/branching-strategy.md.

Superseded: opted out as a discrete docs phase, that work landed alongside Phase 3's workflows instead (PR #638). The number was reused for vnext dev builds to CodeArtifact once split out of Phase 3 (blocked on ops-team#299); see #510 for the current scope.

Branching strategy V1 (superseded by V2)

[!NOTE]
Branch names used below were chosen for clarity, but can be almost anything. Assume the requisite branch protections/settings will be configured to make all of this feasible.

This is a draft suggestion of a simpler, more automatable branching flow for the schema repository. Consider this as a starting point for changes, please call out concerns/questions.

Two branches

We maintain two long-lived branches:

  • main, the integration branch. This is where all normal work lands. It is always in a green, releasable state, but it is not a snapshot of the last release - it tracks what the next release will look like.
  • breaking, a parallel branch where breaking changes accumulate between releases. It stays continuously in sync with main so it never drifts. This is opposed to the current approach main_2026_02_12, where its a long-lived breaking branch per release.
graph LR
    A[feature/*] -->|squash merge\n nonbreaking PR| B[main]
    A -->|squash merge \nbreaking PR | C[breaking]
    B -->|CI auto-rebase\n on every merge| C
    C -->|merge at release\n + tag v1.x.x| B
Loading

How normal work flows

All regular work follows the same pattern:

  1. Branch off main
  2. Open a PR back to main
  3. CI runs automatically and checks your changes are compatible with the breaking branch
  4. If all checks passes and you are up-to-date with main, your PR is squash-merged, your branch history is yours to work however you like, but a single clean commit lands on main
  5. Once merged, breaking is automatically rebased onto the new main

You never need to touch breaking directly.

flowchart TD
    A[PR opens to main] --> B[CI: dry-run check\nsimulate squash onto main\ntest breaking rebase]
    B --> C{conflict?}
    C -->|no| D[Squash merge to main]
    C -->|yes| E[Author rebases feature branch\nonto breaking,\npushes to own branch to pre-emptively resolve]
    E -->|re-run check| B
    D --> F[CI: auto-rebase breaking onto main force-push]
    F --> G{failsafe check: rebase fails?}
    G -->|yes| H[Open GitHub issue\nassign PR author]
    G -->|no| I[Done]
Loading

If your PR fails the compatibility check

Occasionally a change conflicts with work that is staged in breaking. If this happens, CI will post a comment on your PR explaining exactly what to do. The short version: you rebase your own feature branch onto breaking to resolve the conflict, push your branch, and the check re-runs automatically.

You are only ever asked to fix your own branch.

If your PR fails compatibility check post-merge

While unlikely, this failsafe check in CI will automagically open a GitHub issue and assign it to you. Until resolved, the breaking branch will be out of sync with main.

Breaking changes

If your work contains breaking changes, target breaking instead of main when you open your PR. Everything else works the same way: squash merge, CI checks, and so on.

Monthly release

Once a month, the breaking branch is merged into main and the result is tagged as the release. There is no separate release branch, the tag on main is the release.

By shifting merge conflicts left in our process, this should always result in a clean, simple merge (making it easy to automate more of the core release steps in the future).

flowchart LR
    A[breaking\n] -->|merge into main| B[main\nnow includes breaking changes]
    B -->|tag v1.x.0| C[release\ntagged commit on main]
Loading

What this means for contributors

  • Always branch from main
  • Target main for normal changes, breaking for breaking changes
  • Use whatever commit style you like on your branch, only the final squashed commit lands on shared branches
  • If CI flags a conflict, follow the instructions in the comment, it will tell you exactly what to run
  • Never push directly to main or breaking

Metadata

Metadata

Labels

automation 🦾Change/enhance automation of a processenhancementNew feature or requestquestionFurther information is requested

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions