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.

Quick start

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

Inputs

Library inputs

Input Required Description
mode no compare (default), compare-release, dump, appcompat, deps, or stack-check
old-library yes (compare, compare-release) Path to old library, JSON snapshot, ABICC dump, directory, or package
new-library yes Path to new library, binary, directory, or package

Header inputs

Input Required Description
header no Public header file(s) or directory(ies) for both sides (space-separated)
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

Application compatibility inputs (appcompat mode)

Input Required Description
app-binary yes (appcompat) Path to application binary (ELF, PE, or Mach-O)
check-against no Library for weak-mode symbol availability check (no old library needed)
show-irrelevant no Include library changes not affecting the application (default false)
list-required-symbols no List symbols the app requires and exit (default false)

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: c++ or c
gcc-path Path to cross-compiler binary (dump mode only)
gcc-prefix Cross-toolchain prefix, e.g. aarch64-linux-gnu- (dump mode only)
gcc-options Extra flags for castxml (dump mode only)
sysroot Alternative system root (dump and deps modes)
nostdinc false Skip standard include paths (dump mode only)

Full-stack dependency validation (Linux ELF)

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

Output and policy

Input Default Description
format markdown Output format: markdown, json, sarif, html. sarif/html only supported in compare mode; other modes fall back to markdown
output-file Path to write report (auto-set for SARIF)
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, pass the flags via extra-args:

extra-args: '--strict-suppressions --require-justification'

Action behavior

Input Default Description
python-version 3.13 Python version for setup-python
install-deps true Install castxml + gcc automatically
upload-sarif false Upload SARIF to GitHub Code Scanning
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)

Package comparison inputs (compare-release mode)

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
keep-extracted false Keep extracted temp files for debugging
fail-on-removed-library false Exit 8 when a library present in old is absent in new

Outputs

Output Description
verdict compare: COMPATIBLE, SEVERITY_ERROR, API_BREAK, BREAKING, or ERROR. compare-release: COMPATIBLE, API_BREAK, BREAKING, REMOVED_LIBRARY, or ERROR. appcompat: COMPATIBLE, API_BREAK, BREAKING, or ERROR. dump: COMPATIBLE or ERROR. stack-check: PASS, WARN, FAIL, or ERROR. deps: PASS, FAIL, or ERROR.
exit-code compare: 0 (compatible), 1 (severity error), 2 (API break), 4 (ABI break). compare-release: 0 (compatible), 2 (API break), 4 (ABI break), 8 (library removed). appcompat: 0 (compatible), 2 (API break), 4 (ABI break). stack-check: 0 (pass), 1 (warn), 4 (fail). deps: 0 (ok), 1 (missing).
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: napetrov/abicheck@v1
        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: napetrov/abicheck@v1
        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: napetrov/abicheck@v1
        with:
          old-library: abi-baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h

Use GitHub Actions cache for baseline

      - name: Restore cached baseline
        uses: actions/cache@v4
        with:
          path: abi-baseline.json
          key: abi-baseline-${{ github.event.repository.default_branch }}-${{ github.sha }}
          restore-keys: |
            abi-baseline-${{ github.event.repository.default_branch }}-

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

SARIF with GitHub Code Scanning

Upload results to the Security tab so ABI breaks appear as code scanning alerts.

Note

Requires security-events: write permission. On PRs, GitHub only shows new alerts introduced by the PR — existing alerts stay on the default branch and don't clutter the review.

jobs:
  abi-check:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - run: mkdir build && cd build && cmake .. && make

      - uses: napetrov/abicheck@v1
        with:
          old-library: abi-baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h
          format: sarif
          upload-sarif: true

Cross-compilation check (dump mode)

Cross-compilation flags (gcc-prefix, sysroot, gcc-options) are only supported in dump mode. Use mode: dump to generate a baseline from a cross-compiled binary, then compare with a separate step.

      # Step 1: dump ABI snapshot from cross-compiled binary
      - uses: napetrov/abicheck@v1
        with:
          mode: dump
          new-library: build-arm64/libfoo.so
          header: include/foo.h
          gcc-prefix: aarch64-linux-gnu-
          sysroot: /usr/aarch64-linux-gnu
          lang: c
          output-file: baseline-arm64.json

Matrix: multiple libraries

    strategy:
      matrix:
        lib:
          - { name: libfoo, so: build/libfoo.so, header: include/foo.h }
          - { name: libbar, so: build/libbar.so, header: include/bar.h }
    steps:
      - uses: napetrov/abicheck@v1
        with:
          old-library: baselines/${{ matrix.lib.name }}.json
          new-library: ${{ matrix.lib.so }}
          new-header: ${{ matrix.lib.header }}

Matrix: multiple platforms (native scan per OS)

Use native runners to get the best platform-specific signal (Linux/ELF, macOS/Mach-O, Windows/PE):

jobs:
  abi-scan:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            ext: so
          - os: macos-latest
            ext: dylib
          - os: windows-latest
            ext: dll
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      # Build your platform artifact here (example command only)
      - name: Build
        run: |
          echo "build on ${{ matrix.os }}"

      - name: ABI compare (native)
        uses: napetrov/abicheck@v1
        with:
          old-library: baselines/${{ runner.os }}/abi-old.json
          new-library: build/${{ runner.os }}/libfoo.${{ matrix.ext }}
          new-header: include/foo.h
          format: json
          output-file: abi-report-${{ runner.os }}.json

      - name: Upload platform ABI report
        uses: actions/upload-artifact@v4
        with:
          name: abi-report-${{ runner.os }}
          path: abi-report-${{ runner.os }}.json

Post-matrix ABI gate (unified verdict)

After per-platform matrix runs, a gate job downloads all JSON reports and produces one aggregated exit code for the entire workflow:

jobs:
  abi-scan:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            ext: so
          - os: macos-latest
            ext: dylib
          - os: windows-latest
            ext: dll
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: cmake -B build && cmake --build build

      - name: ABI compare (native)
        uses: napetrov/abicheck@v1
        with:
          old-library: baselines/${{ runner.os }}/abi-old.json
          new-library: build/libfoo.${{ matrix.ext }}
          new-header: include/foo.h
          format: json
          output-file: abi-report-${{ runner.os }}.json
          fail-on-breaking: false   # let gate job decide

      - name: Upload platform ABI report
        uses: actions/upload-artifact@v4
        with:
          name: abi-report-${{ runner.os }}
          path: abi-report-${{ runner.os }}.json

  abi-gate:
    needs: abi-scan
    runs-on: ubuntu-latest
    steps:
      - name: Download all ABI reports
        uses: actions/download-artifact@v4
        with:
          pattern: abi-report-*
          merge-multiple: true
          path: abi-reports/

      - name: Aggregate verdicts and gate
        run: |
          pip install abicheck --quiet
          python3 - <<'PYEOF'
          import json, sys, os, glob

          SEVERITY = {"NO_CHANGE": 0, "COMPATIBLE": 0, "COMPATIBLE_WITH_RISK": 0,
                      "API_BREAK": 2, "BREAKING": 4, "ERROR": 4}

          worst = 0
          rows = []
          for path in sorted(glob.glob("abi-reports/*.json")):
              with open(path) as f:
                  data = json.load(f)
              verdict = data.get("verdict", "ERROR")
              platform = os.path.basename(path).replace("abi-report-", "").replace(".json", "")
              rows.append(f"| {platform} | {verdict} |")
              worst = max(worst, SEVERITY.get(verdict, 4))

          table = "\n".join(rows)
          print(f"## ABI Gate\n\n| Platform | Verdict |\n|---|---|\n{table}")

          if worst >= 4:
              print("BREAKING ABI change detected on at least one platform.", file=sys.stderr)
              sys.exit(4)
          elif worst >= 2:
              print("API break detected on at least one platform.", file=sys.stderr)
              sys.exit(2)
          print("All platforms: compatible.")
          PYEOF

Tip

Set fail-on-breaking: false in each matrix job so runners don't fail early. The gate job reads all JSON reports and exits 4 (breaking), 2 (API break), or 0 (compatible).

Skip system dependency installation

If castxml + compiler are already available (custom image, pre-provisioned VM, or conda-forge environment), set install-deps: false:

      - uses: napetrov/abicheck@v1
        with:
          old-library: old.json
          new-library: new.json
          install-deps: false

Example (conda-forge pre-step):

      - name: Install abicheck from conda-forge
        run: |
          conda install -y -c conda-forge abicheck

      - uses: napetrov/abicheck@v1
        with:
          old-library: old.json
          new-library: new.json
          install-deps: false

When comparing two JSON snapshots, no header-analysis toolchain is needed.

Full-stack dependency check on container image update

Validate that updating a base image doesn't break your application's dependency stack. This runs stack-check to compare the binary's full transitive dependency tree across old and new container root filesystems:

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

      - name: Extract old rootfs
        run: |
          mkdir -p /tmp/old-root
          docker export $(docker create old-image:latest) | tar -xf - -C /tmp/old-root

      - name: Extract new rootfs
        run: |
          mkdir -p /tmp/new-root
          docker export $(docker create new-image:latest) | tar -xf - -C /tmp/new-root

      - name: Full-stack ABI check
        uses: napetrov/abicheck@v1
        with:
          mode: stack-check
          new-library: usr/bin/myapp
          baseline: /tmp/old-root
          candidate: /tmp/new-root
          format: json
          output-file: stack-report.json

Exit codes for stack-check: 0 = PASS, 1 = WARN (ABI risk), 4 = FAIL (load failure or ABI break).

Dependency tree audit

Show the resolved dependency tree and symbol binding status for a binary. Useful for auditing which libraries a binary actually loads and detecting missing dependencies before deployment:

      - name: Audit dependencies
        uses: napetrov/abicheck@v1
        with:
          mode: deps
          new-library: build/myapp
          sysroot: /path/to/target/rootfs

Include dependency info in compare

Add follow-deps: true to include the transitive dependency graph and symbol binding information alongside the regular ABI diff:

      - name: Compare with dependency context
        uses: napetrov/abicheck@v1
        with:
          old-library: baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h
          follow-deps: true

Inline PR annotations

Add --annotate to get ABI breaking changes as inline comments on the PR diff. See GitHub PR Annotations for full details.

      - uses: napetrov/abicheck@v1
        with:
          old-library: baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h
          extra-args: --annotate

Conditional failure

Allow API breaks but block binary ABI breaks:

      - uses: napetrov/abicheck@v1
        with:
          old-library: baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h
          fail-on-breaking: true
          fail-on-api-break: false

Detect unintentional API expansion

Block PRs that accidentally add new public symbols or types:

      - uses: napetrov/abicheck@v1
        with:
          old-library: baseline.json
          new-library: build/libfoo.so
          new-header: include/foo.h
          fail-on-breaking: true
          severity-addition: error   # exit code 1 if any new public API appears

When severity-addition: error: - Exit code 1 → new public symbol/type added (verdict: SEVERITY_ERROR) - Exit code 0 → no additions, no breaks (verdict: COMPATIBLE) - Exit code 4 → binary ABI break (verdict: BREAKING)

This is useful when your library has a stable frozen API and any expansion must be a deliberate, reviewed decision rather than an accidental side effect.

Compare RPM packages

Use mode: compare-release to compare all shared libraries inside two packages without manual extraction. Supported formats: RPM, Deb, tar (.tar.gz, .tar.xz, .tar.bz2, .tgz), conda (.conda, .tar.bz2), wheel (.whl), and plain directories.

      - name: Compare RPM packages
        uses: napetrov/abicheck@v1
        with:
          mode: compare-release
          old-library: libfoo-1.0-1.el9.x86_64.rpm
          new-library: libfoo-1.1-1.el9.x86_64.rpm

Compare packages with debug info

Provide separate debug info packages for full type-level analysis via build-id resolution:

      - name: Compare with debug info
        uses: napetrov/abicheck@v1
        with:
          mode: compare-release
          old-library: libfoo-1.0.rpm
          new-library: libfoo-1.1.rpm
          debug-info1: libfoo-debuginfo-1.0.rpm
          debug-info2: libfoo-debuginfo-1.1.rpm

Compare Deb packages with development headers

      - name: Compare Deb packages
        uses: napetrov/abicheck@v1
        with:
          mode: compare-release
          old-library: libfoo1_1.0-1_amd64.deb
          new-library: libfoo1_1.1-1_amd64.deb
          devel-pkg1: libfoo-dev_1.0-1_amd64.deb
          devel-pkg2: libfoo-dev_1.1-1_amd64.deb

Compare tar archives (DSOs only)

      - name: Compare SDK tarballs
        uses: napetrov/abicheck@v1
        with:
          mode: compare-release
          old-library: sdk-2.0.tar.gz
          new-library: sdk-2.1.tar.gz
          dso-only: true

Compare conda packages

      - name: Compare conda packages
        uses: napetrov/abicheck@v1
        with:
          mode: compare-release
          old-library: pkg-v1.conda
          new-library: pkg-v2.conda

Application compatibility check

Check whether your application binary is affected by a library update:

      - uses: napetrov/abicheck@v1
        with:
          mode: appcompat
          app-binary: build/myapp
          old-library: libfoo.so.1
          new-library: build/libfoo.so.2
          header: include/foo.h

Quick symbol availability check (weak mode)

Verify a library provides all symbols an application needs — no old library required:

      - uses: napetrov/abicheck@v1
        with:
          mode: appcompat
          app-binary: build/myapp
          check-against: build/libfoo.so
          install-deps: false

Versioning

The action follows semantic versioning with floating major version tags:

uses: napetrov/abicheck@v1         # latest stable v1.x.x (recommended)
uses: napetrov/abicheck@v1.2.0     # exact version (reproducible)
uses: napetrov/abicheck@abc123def  # exact commit SHA (most secure)

The v1 tag is updated with each patch/minor release. Breaking changes to the action interface will increment to v2.