Skip to content

check-target Action Reference

actions/check-target composes resolve-baseline + collect-facts + the root abicheck/abicheck Action into one resolved check — the single high-level primitive — and, once its own input validation passes, always emits the report envelope (§7), regardless of whether the baseline resolved, was a bootstrap "no baseline yet" pass, a "target new to this baseline-set" pass (allow-new-target: true), or failed outright. An invalid invocation (e.g. a missing required input, or an unsupported input combination) is rejected up front, before any of that, and produces no report or outputs at all.

Status. This page documents the actions/check-target composite Action shipped in G30 P1.3. The reusable workflows that generate a run-plan.json and fan this Action out over a matrix (check-single.yml/check-project.yml, G30 P1.4) are documented separately — see the run-plan schema and the reusable workflows reference. check-target can also still be called directly as a step (the S4 shortcut) or from a hand-written per-target workflow (S1/S2/S5/S6/S15/S21) without either.

What it does

  1. Resolves the baseline by composing resolve-baseline — skipped entirely when baseline-channel: none (a single-build audit with no baseline).
  2. Composes collect-facts when evidence-producer requests build/source evidence: phase: verify for wrapper/clang-plugin (the caller's own workflow must run collect-facts phase: prepare before its build step, earlier in the job — check-target runs after that build already happened and cannot retroactively instrument it), or phase: auto for replay (no pre-build hook needed).
  3. Runs the analysis — the root Action's compare mode against the resolved baseline, or (baseline-channel: none) compare's own audit-only shape (old-library/abi-baseline both omitted, a one-build audit).
  4. Writes the report envelope, once input validation (the very first step) has passed — even when steps 1 or 3 above then fail, since the internal resolve/analysis steps run with continue-on-error: true specifically so this step always runs afterward. A validation failure short-circuits before any of steps 1-4 run at all.
  5. Owns its own composite exit code, per gate-mode (below) — the very last thing this Action does, never an implicit pass-through of an internal step's raw exit code.

gate-mode

Mode This job's exit code Who computes the real gate
local (default) Reflects the real compatibility finding (today's root-Action behavior). This job itself.
deferred 0 for a compatibility finding, whatever it is — but still nonzero on an operational error (a resolve-baseline failure, or the analysis step never producing a report). A trailing fan-in aggregate job, reading this check's real, un-neutralized severity/exit_code from its report.
advisory Same as deferred. Nobody — findings are visible (in compatibility_verdict/policy_gate_decision) but never gate CI (shadow-rollout burn-in, S26).

deferred vs. advisory reports differ in one important way: a deferred report's severity/exit_code block stays the real value — that's exactly what a trailing aggregate job's exit_code() (a max() over every report's real gate) needs to compute the actual gate centrally. An advisory report's legacy severity.exit_code/blocking are neutralized to 0/false so an advisory check can never accidentally raise aggregate's computed exit code above what the required cells alone would produce — the real finding stays fully visible in the new compatibility_verdict/policy_gate_decision fields for humans/PR comments/SARIF, just never in the field aggregate gates on.

Operational errors are never deferred or hidden, regardless of gate-mode: a resolve-baseline failure (not_found/ambiguous/ wrong_profile/stale_schema/incompatible_evidence) or the analysis step never producing a report always fails this job's own exit code, exactly like resolve-baseline's own fail-loud contract requires.

Target kinds

target-kind selects which compare flags this check builds — only meaningful when kind: target (never kind: bundle):

target-kind Compare shape Extra inputs
library (default) A plain compare. —
app-consumer compare --used-by (S22, application compatibility). consumer-binary
plugin-contract compare --required-symbol @FILE (S23, plugin/dlopen contract). contract-file — a .syms file, one required linker symbol per line, # comments allowed; not YAML

The "library redirect": app-consumer/plugin-contract targets have no binary/baseline of their own — they resolve through the library they scope. Set baseline-target to that library's id so resolve-baseline looks up the right baseline, while name stays the contract target's own name (myapp-consumer, ioc-plugin-contract) for this check's own check_id/target_id identity, and new-library points at that library's own candidate binary (candidate lookup needs the same redirect as the baseline lookup — an app-consumer/plugin-contract target has no binary_pattern of its own to resolve either side from).

kind: bundle (S14)

A bundle-scoped check never resolves a single snapshot — resolve-baseline returns the bundle's staged member binaries (binaries-dir), since abicheck/bundle.py's cross-library graph reads real ELF binaries, not JSON snapshots. check-target hands that directory to the root Action's compare mode as old-library directly — a plain directory-operand compare, which compare already fans out to a per-library comparison (including cross-library bundle findings) automatically; no separate "bundle compare" mode or CLI command is invoked. new-library is the caller-provided directory of the candidate build's own member binaries.

Two different questions a bundle answers

Don't conflate these; they are separate axes with separate exit contributions, and either can hold without the other.

Axis Question Owner
Inventory / scope completeness Were the required selected members actually compared? scope.on_incomplete
Analysis assurance For the comparisons that did run, how trustworthy was the evidence? assurance.require_complete

Neither is the compatibility verdict. A release can be scope-complete with partial assurance (every member compared, one missing its headers) or scope-incomplete with complete assurance (one member never supplied, the rest fully analysed).

The assurance fold is max over compared members: an incomplete member is never hidden by complete siblings, and over a one-member package the fold is the identity, so it gates exactly as comparing that one library alone would. See Multi-binary → Analysis assurance across a bundle.

Three bundle paths, three sets of restrictions

"Bundles support X" is not a statement this documentation can make once. Three different paths reach a multi-library comparison and they do not share capabilities:

Path What it is Restrictions
Direct CLI abicheck compare OLD_DIR NEW_DIR (a directory/package operand, fanned out per library) The CLI's own; not bound by this Action's input validation
Stored member / bundle-facts a stored ProjectSnapshot package or a BundleFacts document as an operand Limited by what the capture recorded
Managed kind: bundle this Action, driven by check-project The table below

A managed kind: bundle check is validated up front, before any setup work. These combinations are rejected, and each rejection reflects an evidence path that does not exist — not a documentation choice:

Rejected Why
requested-depth: headers\|build\|source A bundle's baseline is staged raw binaries with no historical per-member header/build/source evidence. Only binary is supported. Use kind: target to compare one library at a deeper rung.
baseline-channel: none There is no no-baseline bundle audit path: a bundle always compares directories, which compare's audit-only shape cannot do.
allow-new-target: true A bundle comparison needs one coherent release where every member already coexisted. Scope a new member with its own kind: target check.

analysis-assurance-complete is not in that list — see its row in Inputs. Supporting a release-level assurance gate is a different capability from supporting historical header-aware bundle capture; the first landed, the second has not.

Inputs

Input Required Default Meaning
kind no target target or bundle.
target-kind no library library | app-consumer | plugin-contract (kind: target only).
name yes — This check's own identity (check_id/target_id) — the target or bundle id.
baseline-target no (= name) Which target's baseline actually resolves — set to the referenced library for app-consumer/plugin-contract. Ignored for kind: bundle.
bundle-members when kind: bundle [] JSON array of the bundle's member target ids.
profile yes — The build profile.id this check runs under.
baseline-channel yes — A channel name, or the literal none for a no-baseline audit (S5) — none is rejected for kind: bundle (a bundle has no no-baseline audit path; it always compares directories, which compare's audit-only shape cannot do).
baseline-path when channel ≠ none '' Forwarded to resolve-baseline.
baseline-required no true Forwarded to resolve-baseline's required.
candidate-build-output no '' Forwarded to resolve-baseline's incompatible_evidence check.
allow-new-target no false Forwarded to resolve-baseline's allow-new-target — false means a target absent from an otherwise-resolved baseline-set always fails ambiguous; true opts this check into the new_target outcome instead, an advisory, non-fatal lifecycle state for a target checked before it has ever been published in a baseline-set (e.g. a new library's first release). Only meaningful for kind: target; rejected outright for kind: bundle (a bundle comparison needs one coherent release where every member already coexisted). Pair with baseline-required: false, or a required-coverage gate would still block on the target's first appearance.
requested-depth yes — binary | headers | build | source — for kind: bundle, only binary is supported (headers/build/source are all rejected: a bundle's baseline is always raw binaries with no historical header/build/source evidence staged per member).
explicit-id no '' (G42 "Explicit check identifiers") This check's checks[].id, if the project declared one — folded into check-id's ~<explicit_id> tail so two checks sharing (name, profile, baseline-channel, requested-depth) but declaring different analysis method/policy/assurance each produce a distinct, non-colliding check-id/report. Omit (default) for the pre-G42, unqualified check-id shape.
build-system / build-generator no '' (WS-A) This cell's build-output.json profile.build_system name and generator. When build-system is set the report envelope carries profile_build_system: {name, generator} (report schema 5.11) in every mode, so the cell's findings name the build lane that produced them. check-project.yml forwards them from the run-plan cell.
gate-mode no local local | deferred | advisory.
project no ${{ github.repository }} Recorded in the report envelope.
head-sha no ${{ github.sha }} Recorded in the report envelope.
base-ref no '' Recorded in the report envelope.
evidence-producer no '' '' (no source evidence needed) | replay | wrapper | clang-plugin.
evidence-pack-path no abicheck_inputs Must match an earlier collect-facts phase: prepare step's own output path (wrapper/clang-plugin only).
new-library yes — Candidate binary (kind: target) or directory of candidate member binaries (kind: bundle).
consumer-binary when target-kind: app-consumer — Forwarded as --used-by.
contract-file when target-kind: plugin-contract — Forwarded as the root Action's required-symbols input (translated to the CLI's --required-symbol @FILE).
require-complete-analysis no false RETIRED (rulings.py deferred-option followup — hard removal, no deprecation window, mirroring the root Action's own require-complete-analysis retirement, which this input forwarded to). The root Action's input it mapped onto is gone: P0.4's orthogonal ANALYSIS_INCOMPLETE axis is config-only now, .abicheck.yml's assurance.require_complete: true, with no CLI or Action-input override. Still declared so a workflow that sets it gets an explicit ::error:: instead of a silently-ignored input. See analysis-assurance-complete below for how checks[].analysis.assurance: complete (product-gaps audit §3) is now enforced instead.
analysis-assurance-complete no false Set to 'true' when this cell declared checks[].analysis.assurance: complete (RunPlanCheck.analysis_assurance, validated at run-plan generation time by project_targets.py's analysis_assurance_gate.py, which still accepts only 'complete'). This is the config-overlay replacement require-complete-analysis names as its successor: since neither a CLI flag nor an Action input can carry assurance.require_complete any more, this Action merges an assurance: {require_complete: true} fragment into whichever build-config the internal analysis step would otherwise read — the same config-only mechanism a project author's own .abicheck.yml line would produce. check-project.yml is the intended caller; a direct check-target caller may also set it explicitly. Supported for kind: bundle too: the release fan-out a bundle check runs folds every compared member's own analysis_assurance with max into the same exit axis a single-library check uses, so a one-member bundle gates identically to a kind: target check and any member that fell short floors the whole bundle. It used to be rejected outright for a bundle; that guard is retired.
header, old-header, new-header, include, old-include, new-include, lang, ast-frontend, gcc-path, gcc-prefix, gcc-options, sysroot, sources, build-info, compile-db, build-config, policy, policy-file, suppress, severity-preset, severity-addition, extra-args, python-version, install-deps, dependency-source no (mirror the root Action) Forwarded straight through to the internal analysis step. dependency-source (G34 Phase C) is what check-project.yml sets per cell from the profile's own dependency_source:; the root Action owns its accepted-value list and its fallback to install-deps.

Outputs

Output Meaning
outcome The resolve-baseline outcome, or skipped when baseline-channel: none.
check-id target@profile#baseline_channel@requested_depth — always includes the depth suffix, even in the common single-depth case. Gains a ~<explicit_id> tail when explicit-id is set (G42), e.g. target@profile#baseline_channel@requested_depth~my-id.
verdict The legacy verdict field: one of the five Verdict values, ERROR (operational failure), NO_BASELINE (bootstrap pass), or NEW_TARGET (allow-new-target: true pass) — the latter two are deliberately not Verdict members, never a compatibility verdict.
compatibility-verdict Mirrors verdict's casing, empty when unavailable (an operational-failure or bootstrap report).
policy-gate-decision pass or fail — this check's own real gate decision, computed before any gate-mode: advisory neutralization.
report-path Path to the final, enriched report JSON.

Report envelope

Every run writes a single JSON report at check-target-report-<name>-<profile>-<baseline_channel>-<requested_depth>-<digest>.json (the exact path is always available via the report-path output — don't hard-code the filename, since running check-target more than once in the same job, e.g. the same target against two baseline channels, would otherwise overwrite an earlier run's report; the trailing <digest> is a 12-hex-char SHA-256 prefix of the original, unsanitized identity tuple, so two identities that collapse to the same slug under the filename's lossy character substitution still produce distinct files — needed because check-project.yml downloads every matrix cell's report into one shared flat directory), starting from whatever the underlying compare run already produced and layering on the fields below. For a normal single-library compare (the common case), that starting shape is abicheck/reporter.py's existing compare-report shape (the one carrying report_schema_version — see Output formats for that contract and abicheck.schemas.current("compare") for the version this build emits). A baseline-channel: none audit instead starts from compare's own audit-only (--no-baseline) report (its own audit_report_schema_version shape), and a kind: bundle check starts from the CLI's per-library release fan-out summary (libraries/old_dir, no schema-version marker of its own) — neither of those two carries report_schema_version.

  • check_id/target_id — always the same, fully-qualified, depth-suffixed value, so abicheck aggregate's exact-match lookup lines up for every check, not only ones sharing a target with another check.
  • profile_id, baseline_channel, requested_depth, effective_depth (may be shallower than requested when the evidence wasn't actually available — check_evidence_coverage records why).
  • compatibility_verdict/policy_gate_decision — the new, richer fields — alongside, never instead of, the legacy verdict/severity fields abicheck/workflows/aggregate/ already parses (the dual-write requirement).
  • operational_errors — non-empty exactly when this check hit an infrastructure/config problem rather than (or in addition to) a compatibility finding.
  • publication — whether/where this report was actually published.

A resolve-baseline failure, a bootstrap ("no baseline published yet") pass, or a new-target ("baseline-set resolved but has no entry for this target yet", allow-new-target: true) pass all synthesize this same envelope from scratch — a report always exists, even when no comparison ever ran.

Example

- name: Check libpvxs against accepted-main
  uses: abicheck/abicheck/actions/check-target@c9e135a3233b6d45e9571533f71293fde458a469  # not yet in a tagged release; pin main or newer
  with:
    name: libpvxs
    profile: linux-x86_64-gcc13-release
    baseline-channel: accepted-main
    baseline-path: ./restored-baseline # staged by an earlier actions/cache step
    requested-depth: headers
    gate-mode: local
    new-library: build/lib/libpvxs.so
    header: headers/pvxs/*.h
# S5: single-build audit, no baseline.
- name: Audit libpvxs (no baseline)
  uses: abicheck/abicheck/actions/check-target@c9e135a3233b6d45e9571533f71293fde458a469  # not yet in a tagged release; pin main or newer
  with:
    name: libpvxs
    profile: linux-x86_64-gcc13-release
    baseline-channel: none
    requested-depth: headers
    gate-mode: advisory
    new-library: build/lib/libpvxs.so
    header: headers/pvxs/*.h