--- name: config-breaking-changes description: >- Review configuration property changes in the Axelix project for breaking changes and migration-policy compliance. Use this skill whenever a diff, PR, or commit touches Spring Boot configuration properties (@ConfigurationProperties classes, application.yml/.properties, spring-configuration-metadata) OR Helm chart values (values.yaml, values.schema.json, templates) — including any rename, removal, type/format change, or default-value change of a property or value. Trigger it even when the user only says "review this PR", "check for breaking changes", "review the config/values changes", or when a diff adds, removes, or renames keys in values.yaml or a configuration-properties class. It catches in-place renames, removal of non-deprecated properties, type/format changes, and default-value changes, and applies the "breaking-change" label when a breaking change is present. --- # Config Breaking-Change Review Configuration properties are a **public contract**. Operators pin them in their `application.yml`, their Helm `values.yaml`, their GitOps repos, and their runbooks. When a property silently disappears, gets renamed in place, stops accepting the value they wrote, or quietly changes its default, their deployment breaks — often only at the worst moment, during an upgrade in production. The whole point of this review is to make sure every config change either preserves that contract or breaks it *loudly, on purpose, and only at a major version boundary, with a migration path the operator can follow.* This skill applies the **same four rules** to two surfaces: - **Spring Boot properties** in the `axelix` application — see `references/spring-boot.md` - **Helm chart values** — see `references/helm.md` A single PR can touch both. Review each surface it changes. --- ## Workflow ### 1. Gather the diff and the version context You cannot judge most of these rules without knowing **what version this change ships in** and **whether a property was already deprecated in a previously released version**. Removal, in particular, is only legal at a major-version boundary for a property that was *already* deprecated in an earlier release. Collect: - **The diff under review.** Prefer the PR diff; otherwise diff the current branch against its base (`git diff ...HEAD`). - **The target version.** For Spring Boot, read the project version (`pom.xml` / `build.gradle`). For Helm, read `Chart.yaml` (`version` and `appVersion`). Note whether this PR/release crosses a **major** boundary (e.g. 1.x → 2.0). - **Deprecation history.** For any property being removed, use `git log`/`git blame` and the changelog to find out *when* it was deprecated. A deprecation added in this same PR does **not** authorize removal in this same PR. If you genuinely cannot determine the version context, say so explicitly in the report rather than guessing — an unverifiable removal is a finding, not a pass. ### 2. Inventory the property changes For each surface the diff touches, identify every property/value that was **added, removed, renamed, retyped, or had its default changed**. The reference files tell you exactly where these live and how to read defaults, types, and deprecation markers on each surface. Read the relevant reference file before classifying. ### 3. Classify each change against the four rules Walk every changed property through the rules below. Each rule defines what a **compliant** change looks like and what counts as a **violation**. Two outcomes are independent and you must report both: - **Violation** — the change breaks the migration policy. The author must fix it before merge. - **Breaking change present** — the change is breaking (even if handled correctly, like a default change at a major bump). This drives the `breaking-change` label. A correctly-executed rename is still a breaking change in spirit and should be labelled; an *incorrectly*-executed rename is both a violation **and** a breaking change. ### 4. Report (see output format below) ### 5. Apply the label if any breaking change is present (see labeling below) --- ## The four rules ### Rule 1 — Renaming a property: never in place; migrate gradually A name is part of the contract. We **never** rename a property in place — not even when bumping the major version — because an operator who upgrades would have their old key silently ignored. Instead a rename is a **gradual migration that spans three major versions**, giving operators a grace period to move over: ``` 1.0.0: Property A 2.0.0: Property A_new AND Property A (old) — A_new takes precedence, falls back to A 3.0.0: Property A_new only ``` **The introducing PR (the 2.0.0 step) must, in the same commit:** 1. **Add the new property.** Its value takes precedence; it falls back to the old property's value only when the new one is unset. (Concretely, renaming `instanceName` → `instanceNamePrefix`: read `instanceNamePrefix`, and only if it has no value, read `instanceName`.) 2. **Deprecate the old property** and point operators at the replacement. **Violation if:** the old name is gone and the new name appears in its place (rename in place); the new property is added but the old one is *not* deprecated; precedence is backwards (old value wins over new); or there is no fallback so existing configs stop working. The final removal of the old property (the 3.0.0 step) is governed by **Rule 2**. ### Rule 2 — Removing a property: deprecate first, communicate the alternative, remove only at a major boundary A property existed for a reason, so we can never *just* remove it. Removal is the last step of a deliberate, announced deprecation: - **It must already have been deprecated in a previously released version.** Deprecating and removing in the same PR is a violation — operators never got the grace period. - **It may only be removed in a new major release.** Removing it in a minor/patch release is a violation. - **The deprecation must have communicated the alternative**, and the removal notice must repeat it. Always answer: *what should operators do instead?* — switch to a replacement property, use a new bundle of finer-grained properties, or (if the feature is genuinely obsolete) explain that no replacement is needed and why. "We're removing X" with no path forward is never acceptable. **Violation if:** a non-deprecated property is removed; a property is deprecated and removed in the same PR; a property is removed outside a major bump; or the removal/deprecation gives no alternative or explanation. ### Rule 3 — Changing a property's type or format: introduce a new property instead We do **not** change the accepted type or format of an existing property, because a config that was valid yesterday must stay valid. If `heartbeatInterval` accepted a number and you now want it to accept a string, do **not** widen or change its type in place — an operator's existing numeric value might now parse differently or fail. Instead, **introduce a new property that accepts the new type** (e.g. `heartbeatIntervalText`) and then run the **Rule 1** migration: new property takes precedence with fallback to the old one, and deprecate the old one in the same commit. The old typed property is eventually retired under Rule 2. **Violation if:** an existing property's type or accepted format changes in place (e.g. a field's type changes, an enum loses or repurposes a value, a parser is swapped for an incompatible one) without a new property carrying the new type. ### Rule 4 — Changing a default value: breaking, but allowed at a major bump with no new property Changing a default **is** a breaking change for us — full stop. An operator relying on the implicit default (heartbeat interval 30 → 45) gets different runtime behavior after an upgrade without changing a single line of their config, and that surprise is exactly what we refuse to ship silently. Unlike the other rules, a default change needs **no new property and no advance signalling**: ship it in a **new major version** and that is fine. **Violation if:** a default changes in a minor/patch release. **Compliant** when it lands in a major release — but it is still a breaking change, so it must be **labelled** and called out in the report (and ideally noted in the changelog/migration guide so operators see the new behavior). --- ## Applying the `breaking-change` label If the review finds **any** breaking change present (a violation, or a compliant-but-breaking change like a major-version default change), the PR must carry the `breaking-change` label so downstream release tooling and reviewers (CodeRabbit, etc.) treat it accordingly. Apply it with whatever is available in the environment: 1. **`gh` CLI**, if installed and authenticated: ```bash gh pr edit --add-label "breaking-change" ``` 2. **GitHub REST API**, otherwise (token in `$GITHUB_TOKEN` / `$GH_TOKEN`): ```bash curl -sS -X POST \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos///issues//labels" \ -d '{"labels":["breaking-change"]}' ``` If neither the CLI nor a token is available (e.g. a purely local review), **do not fail** — state clearly in the report that the PR requires the `breaking-change` label and that it could not be applied automatically, so a human or CI can add it. --- ## Output format Report like this: ``` ## Config breaking-change review **Surfaces reviewed:** **Target version:** () **Breaking change present:** → label `breaking-change` ### Violations (must fix before merge) - `` (): — ### Compliant breaking changes (allowed, labelled) - `` (): ### Notes / unverifiable - ``` If there are no config property changes at all, say so plainly — a one-line "no configuration properties changed in this diff" is the right answer, and don't apply the label.