Skip to content

GitHub Action

abicheck ships as a reusable GitHub Action that you can add to any CI pipeline with a few lines of YAML. It installs Python, system dependencies, and abicheck automatically, then runs ABI comparison and reports results.

Picking a mode or failure policy? See Choose Your Workflow for the decision matrix — which artifacts map to which mode, and which severity inputs gate the build.

Have more than one library, profile, or baseline channel? This page covers the root Action as one step. For a project's whole CI integration lifecycle — multiple targets, build profiles, baseline channels, and the check-project.yml matrix — see Which Scenario Am I?.

Quick start

- uses: abicheck/abicheck@v0.6.0
  with:
    old-library: abi-baseline.json
    new-library: build/libfoo.so
    new-header: include/foo.h

Mode/input compatibility

Not every input is meaningful in every mode. The Action's first step (Validate mode/input combination) checks the combinations below before Python setup, system-dependency installation, or pip install abicheck — an unsupported combination fails immediately with a clear error instead of after a multi-minute toolchain install, and instead of silently falling back to a different, unrequested behavior.

mode: scan itself is retired outright in 0.6 (hard removal) — setting it fails the step immediately, naming the replacement for your shape. See Migrating from mode: scan below if you're updating an existing workflow.

Capability compare, two-sided compare, audit-only dump deps-tree / deps-compare
Single binary/snapshot yes yes (new-library only) yes yes
Directory/package (new-library/old-library) yes (fans out per-library) error error —
Source-only (no new-library, via sources/build-info/compile-db) — — yes —
format: sarif yes (single pair only) yes n/a (always JSON) error
format: html yes (single pair only) error n/a (always JSON) yes (dependency-stack report)
format: review yes (single pair only) error n/a (always JSON) error
format: json yes yes n/a (always JSON) yes
format: markdown yes yes n/a (always JSON) markdown only
format: junit / oneline yes yes n/a (always JSON) error
upload-sarif: true yes (needs format: sarif) yes (needs format: sarif) error error
pr-comment yes yes no-op no-op

For a multi-library release directory (several .so/.dll/.dylib files), use mode: compare with a directory/package operand — it fans out to a per-library comparison automatically (see Package comparison inputs below). dump and compare's own audit-only shape have no such fan-out: dump/audit each library individually (one step per binary, or a matrix) — see Choose Your Workflow for weighing that split against a single combined step. If the release also carries build-emitted source facts (a shared abicheck_inputs/ pack from one build), see Source Scans → Recommended flow: a multi-library release with one shared facts pack for the full matrix-dump-then-compare walkthrough.

Inputs

The tables below group inputs by task, with just enough detail to pick the right ones for your workflow. For the exhaustive, generated field-by-field list (every input/output, its exact default, and full description straight from action.yml), see the GitHub Action Inputs/Outputs Reference.

Library inputs

Input Required Description
mode no compare (default), dump, deps-tree, or deps-compare. scan is retired outright — see Migrating from mode: scan.
old-library no Path to old library, JSON snapshot, ABICC dump, directory, or package (a directory/package fans out to a per-library comparison automatically — no separate mode). Omit it (and abi-baseline) on a compare step to run the audit-only shape (compare --no-baseline) against new-library alone instead.
new-library yes (compare, dump*, deps-tree, deps-compare) Path to new library, binary, or JSON snapshot. Directory/package is compare-only, and only for the two-sided shape — dump and compare's own audit-only shape each analyse exactly one artifact and reject a directory/package with a fail-fast error, before any dependency install. *dump may omit new-library entirely for a source-only dump (sources/build-info/compile-db given instead). See Mode/input compatibility below.

Header inputs

Input Required Description
header no Public header file(s) or directory(ies) for both sides (space-separated) — use old-header/new-header instead when old and new actually declare different headers
old-header no Header file(s) or directory(ies) for old side only
new-header no Header file(s) or directory(ies) for new side only
include no Extra include dirs for castxml (both sides)
old-include no Include dirs for old side only
new-include no Include dirs for new side only

Evidence layers in the Action

The Action drives the same five-layer evidence model as the CLI. The inputs above cover L0 (old-library/new-library), L1 (debug info — embedded, or debug-info1/debug-info2 packages when old-library/ new-library are directories or packages), and L2 (header/include).

The deeper layers — L3 build context, L4 source-ABI replay, and L5 source graphs — are now first-class Action inputs. Use the sources/build-info/compile-db inputs in compare or dump mode and abicheck collects them inline; no separate CLI steps are required. See Source scans below and the Build Info & Sources concept guide.

Application-scoped comparison (appcompat folded into compare --used-by)

There is no separate appcompat mode. Scope a normal compare to what an application actually uses via extra-args:

- uses: abicheck/abicheck@v0.6.0
  with:
    old-library: libfoo-old.so
    new-library: libfoo-new.so
    extra-args: '--used-by myapp'

--used-by <app-binary> (repeatable) runs the full library comparison once and reports the consumer's impact beside it — it never replaces the verdict or the exit code, which always describe the full library. The consumer's own assessment is carried informationally in the report's consumer_scope and used_by blocks. (An earlier design did swap the primary verdict for the app-scoped one; that was reverted — see Application Compatibility.) The OLD/NEW operands may be real library binaries or JSON snapshots that carry binary evidence (a dump of a real library, not headers-only).

The dedicated used-by input (space-separated, mutually exclusive with required-symbol/required-symbols) is available from v0.6.0 on; prefer used-by: myapp over extra-args for the same effect.

Version labels

Input Default Description
old-version old Version label for old library
new-version new Version label for new library

Language and compiler

Input Default Description
lang c++ Language mode for the header backend: c++ or c
ast-frontend auto (resolves to castxml, fail-closed) L2 header-AST frontend (dump mode, and compare mode on any operand shape — single-pair two-sided, audit-only, and directory/package release alike): auto, castxml, clang, or hybrid. Like dump and single-pair compare, this Action folds it into a synthesized .abicheck.yml compile: block forwarded via --config instead (along with gcc-path/gcc-prefix/gcc-options/sysroot/nostdinc/lang, when any of those are also set — combining this group with build-config is supported: the synthesized compile: block merges into a copy of the named build-config, and this Action's input wins on a key conflict). Pass --allow-ast-frontend-fallback/--frontend-context via extra-args for the opt-in castxml→clang fallback or SYCL/DPC++ device context on any mode (set .abicheck.yml's compile.ast_frontend_fallback/compile.frontend_context directly instead if extra-args isn't reaching the mode you need — those two flags have no CLI of their own on any mode). Same as ABICHECK_AST_FRONTEND. See Header-Backend Capabilities for the full resolution contract (fallback triggers, the device-context exception, and how an env pin interacts with both).
gcc-path — Path to cross-compiler binary (dump and compare modes, any operand shape) — see ast-frontend above for how this reaches every mode now that the underlying --compiler CLI flag is gone from all of them
gcc-prefix — Cross-toolchain prefix, e.g. aarch64-linux-gnu- (dump and compare modes, any operand shape) — same note as gcc-path above; a full gcc-path wins if both are set, since the merged compile.compiler config key can only hold one
gcc-options — Extra flags for the header frontend (dump and compare modes, any operand shape) — folds into the same synthesized compile: block ast-frontend above describes. A whitespace-containing flag (e.g. -DMSG="hello world") is rejected with a clear error, since a raw CLI arg isn't subject to compile.options' own one-atom-per-entry, whitespace-free contract.
sysroot — Alternative system root (dump/deps-tree modes, and compare mode on any operand shape) — same note as gcc-path above for dump/compare; deps-tree keeps its own direct --sysroot forwarding, unaffected (it is not part of this compile-context group at all)
nostdinc false Skip standard include paths (dump and compare modes, any operand shape) — same note as gcc-path above

A directory/package (release/bundle) compare operand supports these six inputs too: the per-library fan-out threads this both-sides L2 compile context to every pair's header dump, so the Action forwards the same synthesized compile: block it forwards for a single pair. (It used to reject them for that shape — a guard that outlived the CLI restriction it restated, leaving a release comparison through this Action less capable than the same comparison run through the CLI.) See the GitHub Action Inputs/Outputs Reference for the exact wording.

Full-stack dependency validation (Linux ELF)

Input Default Description
follow-deps false Include transitive dependency graph and symbol bindings in dump/compare output
old-root — Sysroot for the old (baseline) environment (required for deps-compare mode)
new-root — Sysroot for the new (candidate) environment (required for deps-compare mode)
search-path — Additional library search directories (space-separated)
ld-library-path — Simulated LD_LIBRARY_PATH (colon-separated)

Source-scan and build-source evidence (compare / dump modes)

These inputs drive source intelligence — L3 build context, L4 source-ABI replay, and L5 source graphs — through compare (both the two-sided and audit-only shapes), or fold the same evidence into a dump snapshot. L4/L5 need clang (installed automatically by dependency-source: system or conda-forge-clang20 — plain conda-forge, the default, doesn't provision clang); without it collection degrades gracefully and L0–L2 stay authoritative.

Input Modes Description
sources compare (both shapes), dump Source checkout/tree; drives L4 replay and graph collection. With a source-level depth and no compile DB, abicheck auto-detects the build system (CMake/Bazel) and runs the query itself to emit one — no flag, no manual build. For compare, this feeds the new (candidate) side only — the old side's evidence is expected to already be embedded in whatever old-library snapshot was resolved; not applicable to a directory/package operand.
build-info compare (both shapes), dump Out-of-tree L3 context: a build dir, a compile_commands.json, or a collected evidence pack. Same new-side-only note as sources above.
compile-db compare (both shapes; dump folds into build-info) Explicit compile_commands.json path.
build-config compare (both shapes), dump Trusted .abicheck.yml; its build.query runs automatically (operator-supplied = trusted).
allow-build-query — Deprecated and ignored (the --allow-build-query dump flag it fed was always a no-op and has since been removed outright). Kept registered only for back-compat with an existing workflow that still sets it.
depth compare (both shapes), dump Evidence-depth dial: binary, headers, build, or source. Maps to --depth. Omitting it means headers — pin build/source explicitly (there is no risk-driven auto escalation any more).
since compare, two-sided shape only Focus the run's source-evidence scope on files changed vs a git ref (e.g. origin/main). Rejected outright for the audit-only shape (compare --no-baseline does not implement revision-range evidence scoping) and for a directory/package operand.
changed-path compare, two-sided shape only Changed path(s) to focus the run's source-evidence scope on (space-separated; alternative to since). Same audit-only/directory-package rejection as since above.
budget compare, two-sided shape only Time guard (e.g. 15m). The step fails on overflow (verdict: BUDGET_OVERFLOW) — a budget never silently shrinks scope. Rejected outright for the audit-only shape (compare --no-baseline's wall-clock guard isn't wired to that path yet).
against — Retired (0.6, hard removal): this applied only to the now-removed mode: scan. Set old-library (or abi-baseline) under mode: compare instead — setting against is now a hard error naming that replacement.
crosscheck — Retired (0.6, hard removal): scan --crosscheck's KEY=LEVEL promotion syntax is gone along with mode: scan itself. Every cross-source check it used to gate already reaches compare as an ordinary finding — use policy/.abicheck.yml's policy.overrides.<CHANGE_KIND>: error to control one check's severity instead. Setting crosscheck is now a hard error naming that replacement.
risk-rules — Retired (0.6): scan --risk-rules and the risk-driven auto depth escalation it fed are gone. Pin depth: explicitly instead; setting this input is an error.

Consuming build-emitted source facts (wrapper / Clang plugin)

If your product build emits its own abicheck_inputs/ pack — via the abicheck-cc compiler wrapper or the optional Clang plugin (both write the identical schema) — there is no separate ingestion step. Pass the pack directory directly in sources or build-info (compare/dump mode); abicheck auto-detects it and folds it in with no re-parse. The Action does not run the wrapper/plugin itself (that happens in your build). The standalone merge CLI command that used to expose this is gone — see Companion Commands.

Output and policy

Input Default Description
format terminal for a scalar comparison; markdown for a package/release Output format: terminal, markdown, json, sarif, html, junit, review, or oneline. sarif/html/review are only available for a single-artifact compare (old-library or abi-baseline, plus new-library, as a single pair) — a directory/package comparison rejects them with a clear error (choose markdown, json, or junit instead). Within that single-artifact operand, sarif is available for both compare shapes (a two-sided compare and the audit-only shape alike), while html/review are two-sided-report renderers with no audit-only equivalent — compare's audit-only shape (old-library/abi-baseline both omitted) narrows the supported set to json/markdown/sarif/junit/oneline. html is also available in deps-tree/deps-compare (a dependency-stack report). Requesting an unsupported format for the mode/shape is a hard error, raised before any dependency install — it used to silently fall back to a supported format with only a warning, which is unsafe for CI (see Mode/input compatibility).
output-file — Path to write report (auto-set for SARIF)
dry-run false Resolve inputs/config and print what the run would do, without analyzing anything or writing output. Exits 0 for a resolvable preview — but an invalid flag combination or an unsatisfiable requested depth/evidence contract still exits nonzero, same as the real run would (see the inputs reference for the one deliberate exception, an unresolved baseline). Maps to --dry-run; supported by every mode, including compare's audit-only shape.
estimate — Retired (0.6, hard removal): this applied only to the now-removed mode: scan, as a dry-run alias. Use dry-run: 'true' instead, which applies to every mode. Setting estimate is now a hard error naming that replacement.
audit — Retired (0.6, hard removal): this applied only to the now-removed mode: scan, forcing a one-build audit-only run. Under mode: compare, simply omit old-library/abi-baseline to run an audit-only compare --no-baseline; set severity-preset (e.g. default) if this job should still gate on a BREAKING/API_BREAK-classified finding the way mode: scan's own audit mode always did. Setting audit is now a hard error naming that replacement.
policy strict_abi Built-in policy: strict_abi, sdk_vendor, plugin_abi
policy-file — Custom YAML policy file
suppress — YAML suppression file (supports label, source_location, expires)
verbose false Enable debug output

To enable suppression lifecycle enforcement, set it in the repository's own .abicheck.yml — these are config keys, not flags, so they do not go through extra-args:

# .abicheck.yml, committed alongside the workflow
suppression:
  strict: true
  require_justification: true

Action behavior

Input Default Description
python-version 3.13 Python version for setup-python. Accepts free-threaded builds such as 3.15t (see Free-threaded Python)
dependency-source (unset — falls back to install-deps) How to install system dependencies: conda-forge (default), conda-forge-gcc14, conda-forge-clang20 (the only conda-forge source that provisions clang — see the note above), system, or none. See the GitHub Action Inputs/Outputs Reference for the exact per-value breakdown.
install-deps true Deprecated — use dependency-source instead (kept for one release cycle; ignored if dependency-source is set). true (its own default too) maps to dependency-source: conda-forge, false maps to dependency-source: none.
upload-sarif false Upload SARIF to GitHub Code Scanning. Requires format: sarif and mode: compare; any other combination is a hard error raised before any dependency install.
fail-on-breaking true Fail step on binary ABI break
fail-on-api-break false Fail step on source-level API break
severity-preset — Severity preset: default, strict, or info-only (compare mode only)
severity-addition — Severity for additions: error, warning, or info (compare mode only)
extra-args '' Additional CLI arguments passed to abicheck
add-job-summary true Write summary to Job Summary panel (ignored for dump mode)
pr-comment true Post a sticky ABI report comment on the PR (compare mode, including directory/package comparisons and the audit-only shape). No-op outside pull_request / pull_request_target events.
pr-comment-mode update update keeps one comment and edits it in place; new posts a fresh comment each run
pr-comment-on changes When to comment: changes, always, or never — see What the default comments on
pr-comment-detail standard Comment detail: summary, standard, or full
pr-comment-report-artifact-url — Direct URL of an uploaded full-report artifact, shown as Download full report; leave unset unless the upload succeeded
github-token ${{ github.token }} Token for the PR comment and baseline auto-fetch (needs pull-requests: write)

Pull requests from forks

pr-comment is a no-op outside a pull_request or pull_request_target event (action/run.sh accepts both) and needs pull-requests: write, which a fork's pull_request deliberately does not get. Publishing a fork PR's result needs a separate, trusted workflow_run job — see Reporting on fork pull requests for the supported two-workflow split and the report / verify-source-run Actions that implement it.

What the default comments on

pr-comment-on: changes (the default) posts when the run produced something a reviewer must act on:

  • at least one ABI/API change;
  • a library added to, or removed from, a package/directory comparison;
  • an incompletely checked comparison scope;
  • a finding a suppression rule disposed of — "100 removals detected, 100 suppressed by rule X" is a fact, not silence;
  • a material limitation the comparison itself recorded — its coverage_warnings, e.g. "No header/AST data; type-level changes may be missed". A clean run whose analysis was narrowed is a result worth reporting, and it is the result a reviewer is least able to infer from the absence of a comment.

It does not post for routine detector inapplicability. A Linux ELF comparison disables the PE, Mach-O, kABI, SYCL and CPython-extension detectors on every run, forever; those are reported in the comment's collapsed Detector applicability block when a comment is rendered for another reason, and they never cause one.

always comments on every run; never disables the comment unconditionally and is always authoritative. None of these settings changes the compatibility verdict, the gate, or the step's exit code — they decide only whether a comment is written.

What the comment shows

Beside the verdict headline and the breaking/needs-review/safe counts:

  • Evidence — the producer's own confidence, analysis depth and the evidence sources it actually had. When the report states no confidence, none is shown; a confidence level is never invented.
  • Limits on what was checked — the comparison's own coverage_warnings, expanded by default.
  • Detector applicability — collapsed; distinguishes "did not run", "not applicable to these artifacts" and "partial coverage".
  • What changed — an entity-by-operation table (functions / variables / types / enums / …, removed / changed / added). It counts findings, not declarations and not unique symbols, and says so in its own header. The counts come from the complete finding list, before any display cap.
  • Findings — per-symbol rows with the change kind, description, old and new values (including one-sided, zero, false and empty-string values), source location and the kind's impact text. Standard detail groups related findings by enclosing API and always provides an All grouped members block so no member is a dead end; full keeps every finding as its own row, subject to the comment size budget.

When the body would exceed GitHub's 65,536-character comment limit, the per-section row budget is tightened first — keeping per-symbol detail — and only then is the detail level reduced. The headline, identity, exact counts, evidence and scope limitations, the disposition summary and the report links survive every level of shortening, and the omitted-row counts are exact.

Linking the full report

The footer always carries View workflow run. It carries a separate Download full report link only when you pass pr-comment-report-artifact-url, which this Action cannot derive: it uploads nothing itself. Take the value from your own upload step's artifact-url output so the link can never point at an upload that failed:

# 1. Produce the report. `-o json=` writes the file the upload below names.
- uses: abicheck/abicheck@v1
  with:
    old-library: baseline/libfoo.so
    new-library: build/libfoo.so
    extra-args: -o json=abicheck-report.json
    pr-comment: 'false'
# 2. Upload it, so the comment can link to something that exists.
- uses: actions/upload-artifact@v7
  id: report
  with:
    name: abicheck-report
    path: abicheck-report.json
# 3. Post the comment, pointing at the upload's own URL.
- uses: abicheck/abicheck@v1
  with:
    old-library: baseline/libfoo.so
    new-library: build/libfoo.so
    pr-comment-report-artifact-url: ${{ steps.report.outputs.artifact-url }}

Source locations are rendered relative to $GITHUB_WORKSPACE, so rows show include/foo.h:20 rather than a runner-specific absolute path.

Package comparison inputs (compare mode, directory/package operands only)

These only apply when old-library/new-library are directories or packages, rather than a single library each — abicheck detects this from the operands themselves, so there is no separate mode to select.

Input Default Description
debug-info1 — Debug info package for old side (RPM/Deb/tar)
debug-info2 — Debug info package for new side (RPM/Deb/tar)
devel-pkg1 — Development package with headers for old side
devel-pkg2 — Development package with headers for new side
dso-only false Only compare shared objects, skip executables
include-private-dso false Include private (non-public) shared objects
fail-on-removed-library false Exit 8 when a library present in old is proven removed in new (NEW must be a stored ProjectSnapshot package or bundle-facts document whose capture asserted a complete inventory, inventory_complete); an unmatched library under an unproven inventory is an incomplete scope instead, governed by .abicheck.yml's scope.on_incomplete key

keep-extracted (the Action input mirroring --keep-extracted) is gone outright (Phase 7d remainder, one-comparison-product.md §4.1): extraction cleanup is unconditional now, with no config replacement.

Outputs

Output Description
verdict compare, two-sided (single pair or directory/package operands, including --used-by/--required-symbol-scoped runs): COMPATIBLE, COMPATIBLE_WITH_RISK (a real, exit-0 tier for a compatible-but-risky change), SEVERITY_ERROR, COVERAGE_INCOMPLETE, ANALYSIS_INCOMPLETE (single-pair only, with .abicheck.yml's assurance.require_complete: true, when analysis_assurance.status is not "complete" — see the audit-only shape's own entry below for the full description; this orthogonal axis applies to both compare shapes), SCOPE_INCOMPLETE (directory/package operands only: an unchecked selected member under .abicheck.yml's scope.on_incomplete: block, or no comparison completed at all — fails the step unconditionally), ADDITIONS_UNACKNOWLEDGED (single-pair only: .abicheck.yml's acknowledgment.unacknowledged_additions: block and public additions no acknowledgment record covers — orthogonal to the compatibility verdict; fails the step unconditionally; see Change acknowledgment), API_BREAK, BREAKING, REMOVED_LIBRARY (directory/package operands with fail-on-removed-library set, which requires a proven-complete NEW inventory), NOT_COMPARABLE, BUDGET_OVERFLOW, EVIDENCE_CONTRACT_ERROR, or ERROR. compare, audit-only shape (old-library/abi-baseline both omitted): COVERAGE_INCOMPLETE/EVIDENCE_CONTRACT_ERROR/ERROR still apply (COMPATIBLE_WITH_RISK/NOT_COMPARABLE/BUDGET_OVERFLOW do NOT — an audit-only exit-0 run always resolves to AUDIT_CLEAN/AUDIT_RISK/DRY_RUN below instead, never the two-sided compatibility tier), plus AUDIT_GATE (a BREAKING/API_BREAK-classified finding against the candidate's own public surface while a severity preset other than info-only was in effect. Unlike legacy mode: scan, this Action never injects a preset on the caller's behalf — set severity-preset (e.g. default) explicitly to opt in to this gating; see "Migrating from mode: scan" below). Not a two-sided compatibility verdict: an audit reports no additions/removals/compatibility verdict at all, only its own candidate-side findings; exit code 3, fails the step unconditionally), AUDIT_CLEAN (same audit-only shape, exit 0, no candidate-side finding at all), AUDIT_RISK (same audit-only shape, exit 0, a candidate-side finding was detected but this run did not gate on it — distinct from AUDIT_CLEAN so a reviewer can tell "nothing found" apart from "something found, not gating"), DRY_RUN (exit 0, dry-run input true or an effective --dry-run via extra-args: no analysis was performed at all, so there is no candidate-side finding — or absence of one — to claim either way; distinct from AUDIT_CLEAN), ANALYSIS_INCOMPLETE (with .abicheck.yml's assurance.require_complete: true, when analysis_assurance.status is not "complete" — the same orthogonal assurance axis as the two-sided shape, since that config key applies to both compare shapes; not a two-sided compatibility verdict either, since no baseline was compared). dump: COMPATIBLE or ERROR. deps-compare: PASS, WARN, FAIL, or ERROR. deps-tree: PASS, FAIL, or ERROR. This table is a summary — action.yml's own verdict output description is the generated, canonical contract; consult it for the full detail on any value above.
exit-code compare, two-sided: 0 (compatible), 1 (severity error, incomplete contract coverage, incomplete analysis assurance, or — directory/package operands — an incompletely checked comparison scope under .abicheck.yml's scope.on_incomplete: block or no comparison completed at all; the four share the code and are told apart by the report's pre-fold severity.exit_code, contract_coverage_exit_contribution, analysis_assurance.status, and the exit block's scope contributions), 2 (API break), 4 (ABI break), 8 (library proven removed, with fail-on-removed-library and a proven-complete NEW inventory), 16 (NOT_COMPARABLE). compare, audit-only shape: 0 (AUDIT_CLEAN/AUDIT_RISK, or DRY_RUN under dry-run: true), 1 (incomplete contract coverage, incomplete analysis assurance, or — dry-run only — a preview of the exit-7 blocker; the three are told apart the same way as the two-sided shape's own exit 1), 3 (AUDIT_GATE), 7 (evidence-contract error — a pinned --depth/--source-method cause only; the --abi3-targeting-an-unrecognisable-binary cause is two-sided-shape only). deps-compare: 0 (pass), 1 (warn), 4 (fail). deps-tree: 0 (ok), 1 (missing). This table is a summary — action.yml's own exit-code output description is the generated, canonical contract; consult it for the full detail on any value above.
report-path Path to the generated report file (empty when no output file was produced)

Usage examples

Compare two libraries on a PR

name: ABI Check
on: [pull_request]

jobs:
  abi-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build library
        run: mkdir build && cd build && cmake .. && make

      - name: Check ABI compatibility
        uses: abicheck/abicheck@v0.6.0
        with:
          old-library: abi-baseline.json  # committed to repo
          new-library: build/libfoo.so
          new-header: include/foo.h
          new-version: pr-${{ github.event.pull_request.number }}

Save a baseline on release

The baseline is a JSON snapshot of the library's ABI surface. Generate it when you release a version, then compare against it on every PR.

name: ABI Baseline
on:
  release:
    types: [published]

jobs:
  save-baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build library
        run: mkdir build && cd build && cmake .. && make

      - name: Dump ABI baseline
        uses: abicheck/abicheck@v0.6.0
        with:
          mode: dump
          new-library: build/libfoo.so
          header: include/foo.h
          new-version: ${{ github.ref_name }}
          output-file: abi-baseline.json

      - name: Upload baseline as release asset
        uses: softprops/action-gh-release@v2
        with:
          files: abi-baseline.json

Download baseline and compare on PR

      - name: Download baseline from latest release
        run: gh release download --pattern 'abi-baseline.json' --dir .
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Check ABI
        uses: abicheck/abicheck@v0.6.0
        with:
          old-library: abi-baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h

Source scans (build & source evidence)

mode: compare is the one-step entry point for source intelligence: it classifies the PR's changed paths, runs the always-on pattern and cross-source checks plus the pinned evidence depth (L3 build context / L4 source-ABI replay / L5 source graph), and — with old-library/abi-baseline set — compares against it. The full CI recipes — pinning depth, single-release audit, cost estimation, cross-check gating, and the ways to feed build/source evidence into a baseline (dump --sources, build-info, build-emitted packs) — live on their own page:

➡️ GitHub Action: Source Scans & Build Evidence

Migrating from mode: scan

0.6 retired mode: scan outright — a step setting it now fails immediately, before Python setup or any toolchain install, with an ::error:: naming the replacement for your own shape. There is no deprecation window. mode: scan never collapsed to one spelling, so it does not migrate to one either:

  • A baseline scan (against/abi-baseline was set). Replacement: mode: compare with the identical value as old-library (or abi-baseline, unchanged) and the same new-library — the ordinary two-sided shape every example on this page already uses.
  • An audit-only scan (no baseline, or audit: true). Replacement: mode: compare with both old-library and abi-baseline omitted. Omission is the trigger, not a separate flag — this runs a first-class compare --no-baseline against new-library alone. See Source Scans § Single-release audit.

Two migration steps are required, not one:

1. Add severity-preset to keep an audit-only step gating. Legacy mode: scan with no baseline gated a CI job on a BREAKING/ API_BREAK-classified finding by default, unconditionally, no flag needed. mode: compare's own audit-only shape reproduces the identical partition at its own orthogonal exit code, 3, published as the AUDIT_GATE verdict — but that axis is opt-in, activated only by severity-preset (any value except info-only). If your audit-only mode: scan step relied on the default gating (i.e. you did not already pass severity-preset: info-only to opt out), add severity-preset: default (or strict) when you move it to mode: compare — without it, the migrated step always exits 0/passes regardless of what the audit finds. A step that already set severity-preset: info-only needs no change. See the G20-corpus verification transcript in the contributor documentation.

2. Replace format: text with format: markdown, if set. Legacy mode: scan supported exactly two format values, text (its own default) and json — text was rendered internally through compare's own markdown renderer, never a distinct text format of its own. mode: compare has no text format at all — action/validate-inputs.sh rejects it outright, before Python/toolchain install, for both the audit-only shape (json/markdown/sarif/junit/oneline only) and the two-sided shape (json/markdown/sarif/html/junit/review/oneline only). A step that explicitly set format: text under either scan shape must change it to format: markdown (or drop the input entirely -- markdown is mode: compare's own default too) when migrating; a step that never set format (so relied on scan's own text default) needs no change, since both defaults render the identical markdown output.

Three inputs have no mode: compare equivalent and are retired outright, with no replacement: new-library-set (multi-library audit — compare each library individually until package component inventories land), risk-rules (the risk-driven auto depth escalation — pin depth explicitly), and legacy crosscheck's KEY=error promotion syntax (every cross-source check already reaches compare as an ordinary finding; use .abicheck.yml's policy.overrides.<CHANGE_KIND>: error via build-config instead).

More usage recipes

Caching a baseline, SARIF upload, cross-compilation, multi-library/multi-platform matrices, dependency/app-scoped checks, PR-comment tuning, and the directory/package comparison recipes (RPM/Deb/tar/conda) are on their own page:

➡️ GitHub Action: More Recipes

Versioning

The action follows semantic versioning. While abicheck is pre-1.0, pin an exact release tag; a floating major tag is not published yet:

uses: abicheck/abicheck@v0.6.0     # exact release tag (recommended, reproducible)
uses: abicheck/abicheck@abc123def  # exact commit SHA (most secure)

v0.6.0 is the version these examples require, not the latest published tag

This site is built from main, and the examples throughout this guide pin v0.6.0 — the version whose behaviour they document. The latest published release is v0.6.0. Until v0.6.0 is tagged, a workflow copied from here will fail to resolve the action. Either pin v0.6.0 and follow the docs for that release, or wait for the tag. See Upgrading from 0.5 to 0.6 for what differs.

Pin to the commit SHA, not just the tag, whenever the job grants an elevated permission — security-events: write (SARIF/Code Scanning upload), contents: write, id-token: write (OIDC/publishing), or similar. A mutable tag can be repointed to different code after you've reviewed it once; every uses: step in that job (not only abicheck/abicheck — actions/checkout and any upload step too) then runs with that permission's token, so the same rule applies to all of them. Keep the release tag in a trailing comment so the pin stays human-auditable:

uses: abicheck/abicheck@<commit-sha>  # v0.6.0

Released tags are listed on the Releases page. Once abicheck reaches a stable 1.0, a floating v1 major tag updated on each patch/minor release will become the recommended pin.