Skip to content

ADR-022: Baseline Registry and Snapshot Distribution

Date: 2026-03-23 Status: Accepted — not implemented; the slice that once shipped was deleted. The BaselineRegistry protocol, BaselineKey/BaselineMetadata models, the filesystem backend, and the abicheck baseline push/pull/list/delete command group were removed by ADR-043's D4 — which additionally records "recreating a baseline registry, in any form" as an explicit non-goal — and nothing under those names remains in the tree. The git-native backend (default per the original decision below), OCI backend, signing/verification, .abicheck.yml registry config, auto-detection, and the retention/baseline gc command were never implemented. What a user needs this for today is a plain JSON snapshot compared via scan --against OLD (ADR-043 D4's replacement column), plus the CI-facing baseline lifecycle ADR-047/G30 owns — see the amendment below. Treat this ADR as a design record, not current behavior. Verified: main@2e43d53 on 2026-08-04 Decision maker: Nikolay Petrov

Amendment (2026-07-27): the CI-facing half of "baseline lifecycle" this ADR originally scoped — publish a baseline, resolve it in CI, refresh it on main — is now separately covered by ADR-047/G30: release-contract (GitHub Release asset) and accepted-main (Actions cache) channels, actions/resolve-baseline's fail-loud resolution taxonomy, and publish-baseline.yml/update-main-baseline.yml. That work supersedes this ADR's .abicheck.yml registry-config/auto-detection phases for the CI use case specifically — a project on G30's baseline lifecycle does not need this ADR's remaining phases to get a working CI baseline story. This ADR's own remaining backends (git-native, OCI, signing, retention/gc) stay a generic, registry-driven distribution mechanism for non-CI consumers; they should be picked up only against concrete demand, not implemented solely to close out this ADR's original phase list.


Context

Current baseline workflow

abicheck produces JSON snapshots via abicheck dump:

abicheck dump libfoo.so -H include/ -o baseline-v1.0.json
abicheck compare baseline-v1.0.json libfoo-new.so -H include/

Snapshots are versioned (schema_version=3, ADR-015), JSON-serializable, and interchangeable between DWARF-derived and castxml-derived modes (ADR-003). The comparison engine consumes snapshots identically regardless of origin.

What's missing

1. No standard storage/retrieval mechanism.

Teams store baselines ad-hoc: checked into git, uploaded to S3, attached to releases, or generated fresh each CI run. There's no abicheck pull-baseline or standard location convention.

2. No branch/release/tag mapping.

A project typically has multiple active baselines: main, release/1.x, release/2.x. There's no mechanism to associate a snapshot with a branch or release, or to select the correct baseline for a given comparison.

3. No integrity verification.

Snapshots are plain JSON. There's no signature, checksum, or provenance metadata to verify that a baseline hasn't been tampered with or that it was produced by a specific version of abicheck.

4. No lifecycle management.

Baselines accumulate. Old release baselines may be irrelevant. There's no retention policy, no way to list available baselines, no cleanup mechanism.

Design constraints

  • Must work for open-source projects (no paid infrastructure required)
  • Must work offline and in air-gapped environments
  • Must not require a custom server component
  • Should leverage existing infrastructure (git, OCI registries, S3/GCS)
  • Must be optional — abicheck should work without a registry

Options considered

Option Description Trade-off
A: Git-native (branch/tag-based) Store baselines in a dedicated git branch or as release artifacts Works everywhere git works; no extra infra; but limited by git's blob handling
B: OCI registry (ORAS) Store baselines as OCI artifacts Modern, standard, but requires OCI-compatible registry
C: S3/GCS/Azure Blob Object storage with prefix conventions Scalable, but cloud-specific
D: Pluggable with git-native default Registry protocol with multiple backends; git as default Extensible; works offline; cloud backends optional

Decision

1. Registry protocol with pluggable backends

class BaselineRegistry(Protocol):
    """Store and retrieve ABI baseline snapshots."""

    def push(self, key: BaselineKey, snapshot: AbiSnapshot, metadata: BaselineMetadata) -> str:
        """Store a snapshot. Returns a reference ID."""
        ...

    def pull(self, key: BaselineKey) -> tuple[AbiSnapshot, BaselineMetadata] | None:
        """Retrieve a snapshot by key. Returns None if not found."""
        ...

    def list(self, prefix: str | None = None) -> list[BaselineKey]:
        """List available baselines, optionally filtered by prefix."""
        ...

    def delete(self, key: BaselineKey) -> bool:
        """Delete a baseline. Returns True if deleted, False if not found."""
        ...

@dataclass
class BaselineKey:
    """Unique identifier for a baseline snapshot."""
    library: str           # Library name (e.g., "libfoo")
    version: str           # Version or branch (e.g., "1.0.0", "main")
    platform: str          # Target platform (e.g., "linux-x86_64")
    variant: str = ""      # Build variant (e.g., "debug", "ssl-enabled")

    @property
    def path(self) -> str:
        """Registry path: library/version/platform[/variant]"""
        parts = [self.library, self.version, self.platform]
        if self.variant:
            parts.append(self.variant)
        return "/".join(parts)

@dataclass
class BaselineMetadata:
    """Provenance and integrity metadata for a baseline."""
    abicheck_version: str           # Version of abicheck that produced the snapshot
    schema_version: int             # Snapshot schema version (ADR-015)
    created_at: datetime            # ISO 8601 timestamp
    build_context_hash: str | None  # Hash of compile_commands.json / flags used (ADR-020a)
    git_commit: str | None          # Source commit that produced the library
    checksum: str                   # SHA-256 of the serialized snapshot JSON
    signature: str | None           # Optional detached signature (GPG/sigstore)

2. Git-native backend (default)

The simplest backend stores baselines in a dedicated git branch:

Branch: abicheck/baselines

abicheck/baselines/
├── libfoo/
│   ├── 1.0.0/
│   │   └── linux-x86_64/
│   │       ├── snapshot.json
│   │       └── metadata.json
│   ├── 1.1.0/
│   │   └── linux-x86_64/
│   │       ├── snapshot.json
│   │       └── metadata.json
│   └── main/
│       └── linux-x86_64/
│           ├── snapshot.json
│           └── metadata.json
└── libbar/
    └── 2.0.0/
        └── linux-x86_64/
            ├── snapshot.json
            └── metadata.json

Operations:

# Push baseline (creates/updates entry on abicheck/baselines branch)
abicheck baseline push libfoo --version 1.0.0 --platform linux-x86_64 \
    --snapshot baseline.json

# Pull baseline
abicheck baseline pull libfoo --version 1.0.0 --platform linux-x86_64 \
    -o baseline.json

# Compare against registry baseline
abicheck compare \
    --baseline libfoo:1.0.0:linux-x86_64 \
    libfoo-new.so -H include/

# List baselines
abicheck baseline list libfoo
# Output:
#   libfoo/1.0.0/linux-x86_64   2026-03-01  abicheck-0.2.0  abc1234
#   libfoo/1.1.0/linux-x86_64   2026-03-15  abicheck-0.2.0  def5678
#   libfoo/main/linux-x86_64    2026-03-23  abicheck-0.2.0  ghi9012

# Delete old baseline
abicheck baseline delete libfoo --version 0.9.0 --platform linux-x86_64

# Auto-detect: push baseline for current branch
abicheck baseline push libfoo --auto
# Detects: version from git tag or branch, platform from binary

Implementation: uses git worktree or git checkout --orphan to manipulate the baselines branch without affecting the working tree. Commits are atomic.

3. OCI backend (optional)

For teams using container registries, store baselines as OCI artifacts via ORAS conventions:

abicheck baseline push libfoo --version 1.0.0 --platform linux-x86_64 \
    --snapshot baseline.json \
    --registry oci://ghcr.io/myorg/abi-baselines

abicheck baseline pull libfoo --version 1.0.0 --platform linux-x86_64 \
    --registry oci://ghcr.io/myorg/abi-baselines \
    -o baseline.json

Media type: application/vnd.abicheck.snapshot.v3+json

4. Filesystem backend (air-gapped / simple)

Plain directory structure on local or network filesystem:

abicheck baseline push libfoo --version 1.0.0 \
    --snapshot baseline.json \
    --registry file:///shared/abi-baselines

abicheck baseline pull libfoo --version 1.0.0 \
    --registry file:///shared/abi-baselines \
    -o baseline.json

5. Integrity verification

Every pushed baseline includes a SHA-256 checksum in its metadata. On pull, the checksum is verified before the snapshot is used.

Optional GPG or sigstore signing:

# Push with signing
abicheck baseline push libfoo --version 1.0.0 \
    --snapshot baseline.json --sign

# Pull with verification
abicheck baseline pull libfoo --version 1.0.0 \
    -o baseline.json --verify

6. CI integration

Typical CI workflow:

# .github/workflows/abi-check.yml
jobs:
  abi-check:
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: cmake --build build/

      - name: ABI check against baseline
        uses: abicheck/action@v1
        with:
          mode: compare
          baseline: "libfoo:latest-release:linux-x86_64"
          new-binary: build/libfoo.so
          headers: include/

      - name: Update baseline (on release)
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          abicheck baseline push libfoo \
            --version ${{ github.ref_name }} \
            --platform linux-x86_64 \
            --snapshot build/abi-snapshot.json

7. Registry configuration

# .abicheck.yml (project root)
registry:
  backend: git           # "git", "oci", "filesystem"
  # OCI-specific:
  # url: oci://ghcr.io/myorg/abi-baselines
  # Filesystem-specific:
  # path: /shared/abi-baselines

baselines:
  auto_platform: true    # Detect platform from binary
  auto_version: true     # Detect version from git tag/branch
  retention:
    keep_releases: 10    # Keep last N release baselines
    keep_branches: 5     # Keep last N branch baselines
    max_age_days: 365    # Delete baselines older than this

Consequences

Positive

  • Standard workflow for baseline storage and retrieval
  • Git-native default requires no extra infrastructure
  • Pluggable backends support diverse environments (cloud, air-gapped, container registries)
  • Integrity verification catches tampering or corruption
  • Branch/version/platform addressing enables multi-target projects
  • CI integration via --baseline flag simplifies workflows
  • Retention policy prevents baseline accumulation

Negative

  • Git-native backend adds commits to the repository (on a separate branch)
  • Large snapshots in git may bloat repository over time (mitigated by retention)
  • OCI backend adds optional dependency on ORAS client library
  • Signing infrastructure (GPG keys, sigstore) adds operational complexity
  • Multiple backends mean more code to maintain and test
  • Registry configuration file is a new concept to learn

Implementation Plan

Phase Scope Effort
1 BaselineRegistry protocol + BaselineKey + BaselineMetadata models 2-3 days
2 Filesystem backend (simplest, for testing and air-gapped) 2-3 days
3 Git-native backend (orphan branch, atomic commits) 3-5 days
4 CLI: abicheck baseline push/pull/list/delete commands 2-3 days
5 --baseline flag for compare command (pull + compare) 1-2 days
6 Integrity: SHA-256 checksum generation and verification 1 day
7 .abicheck.yml configuration file support 2-3 days
8 Auto-detection (version from git, platform from binary) 1-2 days
9 OCI backend (optional, via ORAS) 3-5 days
10 Signing support (GPG / sigstore, optional) 3-5 days
11 Retention policy + baseline gc command 2-3 days
12 GitHub Action integration updates 1-2 days

Implementation status (as shipped)

Phases 1, 2, 4, and 6 did ship once, and were then deleted wholesale by ADR-043 D4 — so this table records what happened to each phase, not a partially-shipped feature a reader could still use:

Phase Scope Status
1 BaselineRegistry protocol + BaselineKey/BaselineMetadata Shipped, then removed (ADR-043 D4)
2 Filesystem backend Shipped, then removed (ADR-043 D4)
3 Git-native backend Not implemented
4 CLI baseline push/pull/list/delete Shipped, then removed (ADR-043 D4)
5 --baseline flag on compare Not implemented — and scan's own --baseline was itself renamed --against (ADR-043 D5)
6 SHA-256 checksum generation/verification Shipped, then removed with the backend (ADR-043 D4)
7 .abicheck.yml registry config block Not implemented
8 Auto-detection (version/platform) Not implemented
9 OCI backend Not implemented
10 Signing (GPG/sigstore) Not implemented
11 Retention policy / baseline gc Not implemented
12 GitHub Action integration Superseded — ADR-047/G30's release-contract and accepted-main channels (see the amendment above)

The git-native-default framing in the Decision/Consequences sections above describes the original design intent. Do not flip the top-of-file status line back to "implemented" by re-landing phases 1-4: ADR-043 D4 lists recreating a baseline registry as an explicit non-goal, so reviving this design needs a new ADR superseding that decision, not an update to this table.