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:
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):
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:
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
block — allow by default, so no existing run changes):
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 →