Skip to content

Rollout and Governance

Turning a compatibility check on is not a flag day. A project that has never run one has an unknown amount of drift already in it, a team that has never read a compatibility report, and consumers whose expectations nobody has written down. This page is the order to do it in, and the two artefacts — a suppression rule and a policy — that let the check say who accepted what, until when instead of quietly hiding it.

1. Advisory first

Run the check for real, publish the report for real, and fail nothing. The per-target Action has an advisory mode for exactly this; the composite Action has no advisory mode of its own, so every gate it carries has to be switched off by hand:

# actions/check-target: the same check as any scenario, with its gate off
- uses: abicheck/abicheck/actions/check-target@main
  with:
    name: libfoo
    profile: linux-gcc              # a profiles: id from .abicheck.yml
    baseline-channel: accepted-main
    requested-depth: headers
    new-library: build/libfoo.so
    gate-mode: advisory

# the composite Action: relax the break gate and make every category advisory
- uses: abicheck/abicheck@v1
  with:
    old-library: baseline/libfoo.so
    new-library: build/libfoo.so
    new-header: include/
    fail-on-breaking: false
    severity-preset: info-only

fail-on-breaking: false on its own relaxes only the binary-break gate: a severity category configured as error, a contract-coverage or analysis-assurance failure under those opt-in inputs, and a removed library under its own input still fail the step. severity-preset: info-only is what makes every finding advisory, and as an explicit input it outranks a severity: block in .abicheck.yml — which is why the addition: error line in §6 belongs to the gated stage, not to this one. An operational error (a missing baseline, an unreadable binary) still fails either Action, as it should: advisory covers the compatibility verdict, never a broken check.

An advisory run still records its real gate decision in the report; it only never turns it into a red check. Keep whatever gate you have today running beside it until a stretch of real pull requests has produced no finding you could not explain. The scenario is S26; the per-target Action's inputs are in check-target, and the .abicheck.yml topology it reads is the project integration layer.

2. Then gate on the strongest signal only

Flip fail-on-breaking back to its default and leave everything else advisory: a binary break is the one finding nobody argues with. Source-level API breaks, risk findings and additions stay visible in the report and the PR comment, and become gates one at a time — fail-on-api-break when the team has seen a few and agrees they are real, a severity: block when additions need a decision (Report the Surface, Not Only the Breaks). How each knob feeds the exit code, and the two exit-code schemes, are owned by CI Gating.

3. An intentional break is a labelled, reviewed event

A break you mean to ship is still a break: it must be detected, recorded and reviewed, and only the gate is relaxed. The label pattern — intentional-breaking-change on the pull request flips the gate off for that PR and nothing else — does exactly that (S27). Two things never happen: the check is not skipped, because a skipped check leaves the accepted-main baseline stale and every later PR fails against it (Where in the Pipeline § Merge to main); and the release baseline is not touched, because that channel records what shipped, not what was merged (Baseline Management).

The pull request description records what the finding was, which consumers it affects, what the migration is, and which release carries it — the report says what broke, and only the PR can say why that is acceptable.

4. Suppressions are contract statements

A suppression is not "make the finding go away". It is a sentence in the contract: this change is accepted, by this owner, for this reason, until this date. Written that way, a rule is reviewable in the PR that adds it and auditable in the run that applies it:

version: 1
suppressions:
  - namespace: "foo::detail::**"
    reason: "Implementation namespace; not part of the SDK contract (owner: platform team)"
  - symbol: _ZN3foo6LegacyD1Ev
    expires: 2026-12-31
    label: v3-migration
    reason: "Removed in 3.0; consumers on 2.x rebuild before 2027 per the deprecation notice"

Two .abicheck.yml keys hold the team to that shape: suppression.require_justification: true refuses a rule with no reason, and suppression.strict: true turns an expired rule into a failed run instead of a silently re-appearing finding. The suppression audit — which runs automatically whenever --suppress is given, with no token to type — lists what each rule is actually doing: matched nothing (stale), matched a breaking change (worth a second look), expired, or about to:

abicheck compare old.json new.so -H include/ \
  --suppress suppressions.yaml

The rule above with the broad namespace selector has one more property: it will not hide a detail:: change that turns out to be reachable from the public surface. A broad rule matches only unreachable changes unless it says allow_public_break: true, so a private-looking removal that a public inline function still calls stays in the report (case192). A narrow rule naming one symbol is already an audited decision and is not gated this way. File format, every selector, and the reachability values are owned by Suppressions; the reachability rule itself is in Suppressions § Reachability-aware suppression.

5. Policies name the contract shape

Where a suppression accepts one finding, a policy states what kind of contract the library has, and so how a whole class of findings is scored. Three base profiles ship: strict_abi (the default — a shared library with unknown consumers), sdk_vendor (consumers rebuild on a schedule, so some source-level churn is accepted) and plugin_abi (a host/plugin boundary). Ecosystem profiles build on strict_abi where a platform's own documented rules differ — qt_kde_cpp, msvc_pe, mach_o_dylib, rust_c_ffi, glibc_symbol_versioned among them (How System Libraries Stay Compatible gives that last one its full treatment):

abicheck compare old.json new.so -H include/ --policy sdk_vendor

What a policy is for is easiest to see in the internal-change cases: a struct nobody outside the library can name gains a field (case118), loses one (case119) or is reordered (case120), and under public-surface scoping the verdict is NO_CHANGE — the change is real, and outside the contract the policy describes. A custom policy file adds your own internal_namespaces convention and per-kind overrides; a --pack is the same overrides as a versioned document shared across projects, which never outranks a value stated explicitly. Profile contents, the custom file format, and packs are owned by Policy Profiles.

Versioning policy

A policy file may also declare a versioning: block (ADR-066 D4) naming five independent controls: scheme (how version labels order — strict SemVer by default), promise (what compatibility the project claims between two ordered versions — none by default, e.g. a pre-1.0 project that makes no compatibility guarantee at all), support_window and deprecation_window (declared support/deprecation obligations), and enforcement (warn or block). The built-in default is today's behavior — strict SemVer advice, no windows, advisory only — so a policy file that never states versioning: changes nothing:

versioning:
  promise: abi_within_major
  deprecation_window:
    min_releases: 1
  enforcement: block

This never changes what abicheck observed: the finding set, the verdict, and the SemVer/SONAME recommendation stay exactly what they would be without the block (ADR-066 D5 — "policy changes acceptance; it never changes facts"). What it adds is orthogonal: whether this release is acceptable under the declared promise (abicheck.policy.versioning_policy.evaluate_release_acceptance, exposed on the typed ReleaseRecommendation as policy_acceptance — see Output Formats § Release recommendation), and, for a longitudinal history over several stored snapshots (abicheck project history), whether each observed removal was preceded by enough deprecation to satisfy deprecation_window.

Change acknowledgment and the additions review gate

A policy file may also declare an acknowledgment: block (ADR-067 D6), today with one control: unacknowledged_additions (allow, warn, or blockallow by default, so no existing run changes):

acknowledgment:
  unacknowledged_additions: block

This is a separate, orthogonal mechanism from suppression and from versioning: above — see Change acknowledgment for the full acknowledgment-record format and the additions-review gate's exit-code contract. In short: an acknowledgment record names one specific, already-detected finding and a reason it was intentionally accepted; the additions review gate reports (warn) or gates on (block) a public addition that carries no such record, without ever reclassifying the addition itself.

6. A minimal .abicheck.yml

Everything this page has discussed that lives in the config file:

version: 1

severity:
  preset: default          # error on abi_breaking and potential_breaking; additions and quality report only
  addition: error          # ...unless the API is frozen: then an unplanned addition gates too (drop this line for a growing SDK)

suppression:
  require_justification: true   # a rule with no `reason` fails at load time
  strict: true                  # an expired rule fails the run instead of silently reappearing

The severity: block above is itself what selects 0/1/2/4 from the severity tiers rather than the legacy 0/2/4 verdict codes — there is no separate exit_code_scheme: key to set: the scheme is fully automatic, determined by whether a severity setting is in effect at all.

There is no policy: key: the profile is selected per run with --policy (or the Action's policy input), so that the same repository can be checked as a strict shared library in one job and an SDK in another. Every key and its type is in the Config Keys Reference; what the severity categories mean is owned by Severity.


Ladder:Report the Surface, Not Only the Breaks · Step 7 · In Practice · Triage a Suspicious Finding