Skip to content

Proto Release Flow

The system-proto repository follows the same queued CI/CD pipeline as documentation and arda-frontend-app, with one additional constraint that is specific to schema repositories: wire-number stability. This guide covers the full release flow end-to-end, including the escape hatches and the lessons learned applying it for the first time during PDEV-662.

For multi-repo release ordering and composite-build safety, see Release Lifecycle. This guide focuses on the governance rules specific to proto-defining repos.

system-proto uses the same PR-body CHANGELOG model as documentation:

  1. Do not edit CHANGELOG.md directly. The changelog-check CI workflow rejects PRs that touch CHANGELOG.md unless the manual-changelog label is set.
  2. Put your entry in the PR description under a ## CHANGELOG heading. The changelog-assembly workflow extracts it on merge.
  3. On merge, changelog-assembly computes the next semver from the category keywords, prepends a release block to CHANGELOG.md, creates a git tag, and — in system-proto’s case — pushes the tagged digest to the Buf Schema Registry (BSR).

The repo’s PR template carries the ## CHANGELOG placeholder with the valid categories pre-listed. An empty or missing block causes changelog-check to fail.

The changemap.json drives version bumps. Choose the category that accurately reflects the nature of the change, not the largest category that would look impressive:

CategorySemver impactProto use
ChangedMajorRenaming a field or message; changing field semantics; altering the discriminator key
RemovedMajorRemoving a message, field, or enum value
AddedMinorNew enum value; new optional field; new message
DeprecatedMinorMarking a field or message deprecated
FixedPatchCorrecting a comment; fixing a buf.validate annotation
SecurityPatchSecurity-relevant annotation or validation fix

Order entries from largest to smallest impact within a release block: Changed/Removed first, then Added/Deprecated, then Fixed/Security. This matches the changelog reader’s mental model — the most disruptive changes first.

This is the proto-specific rule with no parallel in non-schema repos. In Protocol Buffers, the field number of a message field and the numeric value of an enum entry are part of the wire format. These numbers are embedded in serialized bytes. If you renumber them, existing serialized data will silently mis-decode: an AED currency might be deserialized as CAD with no error.

Safe pattern: append new entries at the next-higher number; never change existing numbers.

The Currency enum in money.proto grew from 2 entries (USD, EUR) to 18 entries across two PRs. The correct approach preserved the original numbers exactly:

enum Currency {
CURRENCY_UNSPECIFIED = 0;
CURRENCY_USD = 1; // ← preserved from original
CURRENCY_EUR = 2; // ← preserved from original
CURRENCY_CAD = 3; // ← new entries appended in
CURRENCY_GBP = 4; // alphabetical order by ISO name,
CURRENCY_JPY = 5; // but at the next available numbers
CURRENCY_AUD = 6;
CURRENCY_CNY = 7;
CURRENCY_INR = 8;
CURRENCY_RUB = 9;
CURRENCY_BRL = 10;
CURRENCY_ZAR = 11;
CURRENCY_MXN = 12;
CURRENCY_KRW = 13;
CURRENCY_SGD = 14;
CURRENCY_HKD = 15;
CURRENCY_NZD = 16;
CURRENCY_CHF = 17;
CURRENCY_AED = 18; // ← last, appended at the end
}

USD = 1 and EUR = 2 were never touched. All 16 new entries were appended at positions 3–18, regardless of alphabetical order among the originals. The key insight: the enum values are used in wire bytes, not the names. Wire bytes that carried 1 before the change still decode as CURRENCY_USD after it.

Before every PR that touches a proto file, run:

Terminal window
npm run buf:breaking

This validates the proposed change against the current main branch. Adding new enum values passes; renaming a field fails; changing the name-to-number mapping (e.g., swapping CURRENCY_USD = 1 and CURRENCY_EUR = 2) is caught by the ENUM_VALUE_SAME_NAME rule in system-proto’s FILE rule set (see buf docs).

The tool does not catch higher-level semantic invariants — e.g., “the field at the largest enum number is always the most recently added currency” is a documentation/review invariant, not a proto-level one. Treat the never-renumber rule above as the invariant, and use code review to enforce semantic conventions the tool cannot see.

When a genuine breaking change is required — for example, removing a deprecated field after completing an expand-migrate-contract cycle — the manual-breaking-change label provides the escape hatch:

  1. Coordinate with all consumers first. All services that consume the proto must have a migration plan in flight before the breaking change PR is opened.
  2. Add the manual-breaking-change label to the PR. This bypasses the buf breaking gate.
  3. Document the change under ### Changed or ### Removed in the PR body’s ## CHANGELOG section. Do not use ### Added to describe a removal.
  4. Do not remove the label to fix CI. If buf breaking fails and you remove the label to make it pass without addressing the breakage, you have shipped a silent wire corruption. If the breakage is intentional, keep the label. If it is unintentional, fix the proto.

The manual-changelog label allows direct edits to CHANGELOG.md — bypassing the assembly workflow’s ownership of that file. Use cases are narrow:

  • Correcting a factual error in a historical release entry.
  • Bootstrapping the first-ever release entry before the assembly workflow has run (see First-Release Bootstrap below).

Add this label only when the PR body’s ## CHANGELOG section genuinely cannot express what you need. The assembly workflow’s edit history is the authoritative source; hand-edits diverge from it.

system-proto uses a merge queue so that concurrent PRs do not race to write CHANGELOG.md or push conflicting tags. The flow is:

The PR lifecycle goes through three distinct gates: changelog-check validates the PR body on open/push; the author then adds the PR to the merge queue; a merge_group event reruns all required checks against the latest main to confirm the PR still passes; on success, GitHub merges the PR and the changelog-assembly workflow fires, writing CHANGELOG.md, tagging the release, and pushing to BSR.

PlantUML diagram

Required checks at each gate:

  • PR open/push: changelog-check, buf-lint, buf-breaking
  • Merge queue (merge_group): same set re-validated against latest main
  • Post-merge (assembly): no required checks — assembly is unconditional on merge to main

Do not merge directly via GitHub’s “Merge pull request” button if the repo uses a merge queue. Always use “Add to merge queue” — the queue’s merge_group event provides the final integration test against the latest state of main before the commit lands.

Consumers of system-proto reference a specific version tag, not main:

operations/build.gradle.kts
BufInput.Remote("arda-system", "buf.build/arda-cards/system-proto:v1.0.0")

Pinning :vX.Y.Z rather than :main gives reproducible builds — every ./gradlew generateProto produces the same generated sources regardless of what has since merged to system-proto. It also makes the intent of a consumer bump explicit in the PR diff: reviewers see the version change from v1.0.0 to v1.1.0 and can inspect the system-proto release notes for that delta.

Bump workflow for consumers:

  1. A new system-proto release is published (tag + BSR push via assembly).
  2. In the consumer’s worktree, edit the tag string in build.gradle.kts (or equivalent).
  3. Run buf mod update to regenerate buf.lock against the new tagged digest.
  4. Commit both build.gradle.kts and buf.lock in the same commit.
  5. Reference the system-proto release notes in the consumer PR’s ## CHANGELOG entry so reviewers can follow the chain.

When a repo has never published a release, the assembly workflow has no previous CHANGELOG.md to prepend to. Bootstrap the first release with the manual-changelog label:

  1. Hand-edit CHANGELOG.md — add a ## [1.0.0] release block at the top covering all features already in main.
  2. Add the manual-changelog label to the PR.
  3. Merge via merge queue as usual.
  4. Assembly will run but will skip the prepend step (the file is already consistent); it will still create the git tag and BSR push.

system-proto PR #13 used this approach. The first release block was hand-authored to cover the content of PR #4 (the proto file and 18-currency enum), which had merged earlier without a CHANGELOG entry because the assembly workflow was not yet installed.

After bootstrap, all subsequent releases use the standard PR-body model with no manual-changelog label.

The PDEV-662 project was the first production use of this release flow for system-proto. Three things are worth recording as illustrative examples:

Case-sensitive clq title matching. The changelog-check workflow uses clq, which validates category titles case-sensitively. An entry under ### added (lowercase) passes local grep checks but fails the CI gate. Always use the exact capitalizations listed in the PR template: Added, Changed, Fixed, etc.

App-install prerequisite for bypass actors. The changelog-assembly workflow needs to push to a protected branch (main) and create tags. It runs as the arda-changelog-bot GitHub App, which must be installed on the repo and granted the Bypass branch protection permission in the repo’s ruleset before the first assembly run. A missing App install produces a 403 Resource not accessible by integration error on the push step — the fix is an install, not a token rotation.

Wire-number near-miss. During review of system-proto PR #4, a reviewer noticed that a proposed reordering of the Currency enum would have inserted CURRENCY_AED at position 2, displacing CURRENCY_EUR to position 3. The existing wire bytes \x02 would have decoded as AED instead of EUR for any data serialized before the deploy. The fix was to append CURRENCY_AED = 18 at the end. buf breaking did not catch this because the names were not removed — only the number-to-name binding changed. The wire-stability rule caught it.

  • Release Lifecycle — multi-repo merge ordering, composite-build safety, and worktree cleanup.
  • Polymorphic REST DTO — the kind-discriminator pattern for sealed types on REST boundaries, which follows an analogous contract-first discipline.
  • Money — the domain model that drove the system-proto v1.0.0 expansion.

Copyright: (c) Arda Systems 2025-2026, All rights reserved