Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8bf1823
ci: add ABICheck shadow ABI scan
napetrov Aug 25, 2026
0b98079
ci: require comparable ABICheck shadow evidence
napetrov Aug 26, 2026
24702cc
ci: install CastXML from conda-forge
napetrov Aug 26, 2026
3201533
ci: collect target-specific ABICheck evidence
napetrov Aug 26, 2026
60f7603
ci: retain ABICheck target failures
napetrov Aug 26, 2026
d95ba93
ci: record pvxs C++ dialect in scan evidence
napetrov Aug 26, 2026
8fe7884
ci: override CastXML dialect for GCC 13
napetrov Aug 26, 2026
2a26a19
ci: allow complete pvxs shadow scans
napetrov Aug 27, 2026
839b511
ci: stage PVXS public headers for shadow ABI scan
napetrov Sep 11, 2026
d2d4c61
ci: harden PVXS ABICheck shadow integration
napetrov Sep 11, 2026
3825f0d
ci: fail closed on invalid ABI evidence
napetrov Sep 12, 2026
d0b31ba
ci: separate public and generated PVXS headers
napetrov Sep 12, 2026
c53cc09
ci: stage declared PVXS header sources
napetrov Sep 12, 2026
b8a557d
ci: escape ABI summary references
napetrov Sep 12, 2026
f4f576b
ci: fail closed when staging ABI reports
napetrov Sep 12, 2026
c4de3ae
ci: use L2 ABI scan in shadow workflow
napetrov Sep 13, 2026
e955819
ci: update ABICheck scanner to latest main
napetrov Sep 13, 2026
07c17aa
ci: use current ABICheck export syntax
napetrov Sep 13, 2026
d1023b8
ci: refresh ABICheck main pin
napetrov Sep 13, 2026
a5c52a6
ci: correct ABICheck main revision pin
napetrov Sep 13, 2026
6b096a1
ci: reuse the normal build for the ABI check and publish it safely
napetrov Sep 16, 2026
f1cc10a
ci: read the snapshot path from the manifest's own snapshot field
napetrov Sep 16, 2026
8ffffa8
ci: fix baseline eligibility, report identity and the incomplete cont…
napetrov Sep 16, 2026
3732bc9
ci: drop the redundant ABI capture failure marker
napetrov Sep 16, 2026
2f9c269
ci: make the explicit baseline overwrite path actually work
napetrov Sep 16, 2026
0beca2b
ci: keep per-target detail reachable, and pin the reviewed publisher
napetrov Sep 16, 2026
51b33f0
ci: repair the workflow file broken by an invalid env context
napetrov Sep 16, 2026
440fa8e
ci: order the sticky comment by the producer run, and refresh the pin
napetrov Sep 16, 2026
65d3f8d
ci: pin the whole integration to the merged abicheck revision
claude Sep 16, 2026
d2c3904
ci: hand the generic ABI machinery back to abicheck
claude Sep 16, 2026
22ca9d5
Merge remote-tracking branch 'origin/master' into abicheck-shadow-int…
claude Sep 16, 2026
7143f5d
docs: record the first real-runner validation of the migrated Actions
claude Sep 16, 2026
b13f69d
ci: fix four defects the review found in the migrated integration
claude Sep 16, 2026
d97e677
ci: repin to pick up the report fix that made the ordering guard work
claude Sep 16, 2026
a4965ce
docs: record why the upstream baseline publisher does not fit yet
claude Sep 16, 2026
ea4f758
ci: enforce the default-branch restriction the bootstrap only claimed
claude Sep 16, 2026
e27eb5f
docs: state that no real baseline comparison has run on this branch
claude Sep 17, 2026
08b895b
ci: verify a baseline-set's revision before publishing it under a tag
claude Sep 17, 2026
5a129fa
ci: bind the spec as JSON, verify the analysed commit, and say what ran
claude Sep 17, 2026
f5ade4a
ci: retest extraction on the CI toolchain, and actually compare somet…
claude Sep 17, 2026
40c8eb8
ci: drop a renderer step that cannot load, and record why
claude Sep 17, 2026
bc8e0a1
Merge master into abicheck integration branch
napetrov Sep 17, 2026
e4c5428
ci: consolidate the abicheck integration onto supported owners
napetrov Sep 17, 2026
daffed4
ci: declare the expected checks independently of comparison execution
napetrov Sep 17, 2026
7cbd1c3
ci: stop pre-creating the baseline channel directories
napetrov Sep 17, 2026
d4023bf
ci: repin abicheck to current main and drop default-restating inputs
napetrov Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .ci-local/abicheck-components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"name": "libpvxs",
"artifact": "lib/${EPICS_HOST_ARCH}/libpvxs.so*",
"header": [
"include/pvxs/*.h",
"include/pvxs/versionNum.h"
],
"header_exclude": [
"include/pvxs/iochooks.h"
],
"include": [
"include",
"${EPICS_BASE}/include",
"${EPICS_BASE}/include/os/Linux",
"${EPICS_BASE}/include/compiler/gcc"
]
},
{
"name": "libpvxsIoc",
"artifact": "lib/${EPICS_HOST_ARCH}/libpvxsIoc.so*",
"header": [
"include/pvxs/iochooks.h"
],
"include": [
"include",
"${EPICS_BASE}/include",
"${EPICS_BASE}/include/os/Linux",
"${EPICS_BASE}/include/compiler/gcc"
]
}
]
30 changes: 30 additions & 0 deletions .ci-local/abicheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Extraction context for the advisory abicheck shadow check.
#
# This describes how the PUBLIC HEADERS are parsed for ABI/API extraction.
# It does not change how PVXS itself is built: the libraries analysed are
# exactly the ones `cue.py build` produced, with PVXS's normal flags.
compile:
options:
# PVXS's supported consumer language mode is C++11, and that is what its
# own build uses. Parsing the public headers in C++11 or C++14 against
# the runner's libstdc++ 13 is not possible with the supported CastXML
# (0.7.0, Clang 20): libstdc++ 13's <bits/char_traits.h> fails with
# "statement not allowed in constexpr function" in both modes. Measured
# on 2026-09-16; a conda-forge GCC 16 libstdc++ is worse (it needs C++20
# to parse its own <type_traits>).
#
# This is therefore a deliberate, disclosed deviation of the extraction
# context from the consumer language mode, not a silent one, and it is
# applied identically to libpvxs and libpvxsIoc so the two components and
# both sides of every comparison are interpreted the same way. Remove it
# as soon as a toolchain that parses the headers in C++11 is available.
- -std=c++17
#
# Deliberately NOT set here: PVXS_API_BUILDING (a library-build-only
# macro) and PVXS_ENABLE_EXPERT_API (an opt-in expert surface). The
# headers are parsed the way an ordinary consumer sees them.
assurance:
# A comparison that could not complete must be reported as incomplete, not
# as a clean result. The shadow gate stays advisory; this controls what
# the analysis is allowed to claim, not whether CI goes red.
require_complete: true
158 changes: 158 additions & 0 deletions .github/actions/abicheck-capture/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: 'PVXS ABI capture'
description: >-
Capture ABI/API evidence for libpvxs and libpvxsIoc from an installation
that a normal PVXS build has already produced. Performs no build, no
comparison and no reporting.

What is PVXS-specific lives here: where the EPICS dependency was prepared
and which host architecture it built for. Everything else -- selecting the
real shared object out of its SONAME alias chain, checking it is an ELF
ET_DYN, agreeing the target machine between components, expanding the owned
header sets and refusing a stale exclusion -- belongs to abicheck's own
baseline Action, which reads the component declaration in
.ci-local/abicheck-components.json.

Used by the selected matrix leg of ci-scripts-build.yml (the candidate
capture) and by abicheck-baseline.yml's one-time historical bootstrap, so
the two produce identical baseline-sets from one definition.

inputs:
top:
description: >
Root of the built PVXS tree to capture from. Defaults to the workspace;
the historical bootstrap builds into its own directory and points this
at it.
required: false
default: ''
output-dir:
description: 'Directory to write the baseline-set into.'
required: true
project-ref:
description: >
The revision actually built and analysed, recorded in the manifest.
For a pull_request build this is the merge commit, not the PR head.
required: true
profile:
description: 'Build-profile identifier recorded in the manifest.'
required: true
baseline-generation:
description: 'Scanner-compatibility generation recorded in the manifest.'
required: false
default: '1'
build-config:
description: 'Path to the abicheck extraction config.'
required: false
default: '.ci-local/abicheck.yml'
component-spec:
description: 'Path to the PVXS component declaration.'
required: false
default: '.ci-local/abicheck-components.json'
abicheck-ref:
description: 'Immutable abicheck revision the capture runs from.'
required: true

outputs:
baseline-path:
description: 'The baseline-set directory that was written.'
value: ${{ steps.capture.outputs.baseline-path }}
content-digest:
description: "Digest over the manifest's artifact list."
value: ${{ steps.capture.outputs.content-digest }}
resolved-libraries:
description: 'The concrete artifact/header/include set that was dumped.'
value: ${{ steps.capture.outputs.resolved-libraries }}
machine:
description: 'The ELF machine both components agreed on (e.g. EM_X86_64).'
value: ${{ steps.capture.outputs.machine }}

runs:
using: 'composite'
steps:
# The only project-specific knowledge left: where cue.py put EPICS Base
# and which host arch it built for. These are build-system facts, not
# ABI facts, so abicheck deliberately hard-codes neither.
- name: Resolve EPICS build context
id: epics
shell: bash
env:
TOP: ${{ inputs.top || github.workspace }}
SPEC_IN: ${{ inputs.component-spec }}
WORKSPACE: ${{ github.workspace }}
SPEC_OUT: ${{ runner.temp }}/abicheck-components.resolved.json
run: |
set -eu

# The declaration belongs to the CHECKOUT, not to the tree being
# captured. The historical bootstrap points `top` at an old
# revision that predates this file entirely, so resolving a
# relative spec path after `cd "$TOP"` would read a file that is
# missing (or, worse, a stale declaration) from that revision.
case "$SPEC_IN" in
/*) spec_in=$SPEC_IN ;;
*) spec_in=$WORKSPACE/$SPEC_IN ;;
esac
[ -f "$spec_in" ] || {
echo "::error::component declaration $spec_in does not exist"
exit 64
}

cd "$TOP"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# configure/RELEASE.local is where cue.py records the prepared
# dependency. We read it; we never rewrite it.
if [ -z "${EPICS_BASE:-}" ] && [ -f configure/RELEASE.local ]; then
EPICS_BASE=$(sed -n -E 's/^[[:space:]]*EPICS_BASE[[:space:]]*=[[:space:]]*//p' \
configure/RELEASE.local | tail -n 1)
fi
[ -n "${EPICS_BASE:-}" ] || {
echo "::error::EPICS_BASE is not set and configure/RELEASE.local does not name it"
exit 64
}

EPICS_HOST_ARCH=${EPICS_HOST_ARCH:-}
if [ -z "$EPICS_HOST_ARCH" ] && [ -x "$EPICS_BASE/startup/EpicsHostArch" ]; then
EPICS_HOST_ARCH=$("$EPICS_BASE/startup/EpicsHostArch")
fi
case "$EPICS_HOST_ARCH" in
linux-*) ;;
*)
echo "::error::this capture is declared for a native Linux host arch, got '${EPICS_HOST_ARCH:-<unset>}'"
exit 64
;;
esac

# Render the two build-system values into the declaration. Exactly
# these two placeholders are substituted -- not envsubst, which is
# not guaranteed on every runner image and would also expand any
# other '$' the declaration might legitimately contain.
sed -e "s|\${EPICS_BASE}|$EPICS_BASE|g" \
-e "s|\${EPICS_HOST_ARCH}|$EPICS_HOST_ARCH|g" \
"$spec_in" > "$SPEC_OUT"
if grep -q '\${' "$SPEC_OUT"; then
echo "::error::unsubstituted placeholder left in $SPEC_OUT"
grep -n '\${' "$SPEC_OUT" >&2
exit 64
fi

echo "epics-base=$EPICS_BASE" >> "$GITHUB_OUTPUT"
echo "host-arch=$EPICS_HOST_ARCH" >> "$GITHUB_OUTPUT"
echo "spec=$SPEC_OUT" >> "$GITHUB_OUTPUT"
echo "resolved component declaration:"
cat "$SPEC_OUT"

- name: Capture ABI snapshots (libpvxs, libpvxsIoc)
id: capture
uses: abicheck/abicheck/actions/baseline@80cf72abb5856eb623a6056fb35d06a66ad26774
with:
library-spec: ${{ steps.epics.outputs.spec }}
library-root: ${{ inputs.top || github.workspace }}
output-dir: ${{ inputs.output-dir }}
project-ref: ${{ inputs.project-ref }}
profile: ${{ inputs.profile }}
depth: headers
build-config: ${{ inputs.build-config }}
baseline-generation: ${{ inputs.baseline-generation }}
generator-action-ref: ${{ inputs.abicheck-ref }}
# A libpvxs snapshot is ~124 MB of JSON at this depth (measured).
# The decoded content is identical either way.
snapshot-compression: zstd
101 changes: 101 additions & 0 deletions .github/actions/abicheck-publish-baseline/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: 'PVXS ABI baseline publication'
description: >-
Package a baseline-set and attach it to a release, after checking that
the set really describes the profile it is being published as.

Shared by abicheck-baseline.yml's automatic and bootstrap paths so the
eligibility rules exist once.

inputs:
baseline-path:
description: 'Baseline-set directory (manifest.json plus snapshots).'
required: true
profile:
description: 'Profile this set is being published as.'
required: true
tag:
description: 'Release tag to attach the asset to.'
required: true
overwrite:
description: >
Replace an existing asset of the same name. A published baseline is
an immutable reference: replacing it silently changes the meaning of
every comparison already made against it.
required: false
default: 'false'
github-token:
description: 'Token with contents: write for the release upload.'
required: true

runs:
using: 'composite'
steps:
# actions/stage-baseline packages whatever it is given and explicitly
# does not check the manifest's own profile against the name it is
# published under, so a mismatch would produce an asset that
# resolve-baseline later rejects as wrong_profile for every consumer.
# Check it here, before anything is uploaded.
- name: Check the set describes the profile it is published as
shell: bash
env:
BASELINE_PATH: ${{ inputs.baseline-path }}
EXPECTED_PROFILE: ${{ inputs.profile }}
run: |
set -euo pipefail
manifest="$BASELINE_PATH/manifest.json"
test -s "$manifest" || { echo "::error::no manifest.json in $BASELINE_PATH"; exit 1; }
actual=$(jq -r '.profile // empty' "$manifest")
if [ "$actual" != "$EXPECTED_PROFILE" ]; then
echo "::error::baseline-set records profile '$actual' but is being published as '$EXPECTED_PROFILE'"
exit 1
fi
libs=$(jq -r '[.artifacts[].library] | sort | join(",")' "$manifest")
if [ "$libs" != "libpvxs,libpvxsIoc" ]; then
echo "::error::baseline-set covers '$libs', expected both libpvxs and libpvxsIoc"
exit 1
fi

- name: Package the baseline-set
id: stage
uses: abicheck/abicheck/actions/stage-baseline@80cf72abb5856eb623a6056fb35d06a66ad26774
with:
baseline-path: ${{ inputs.baseline-path }}
asset-name-template: 'abicheck-baseline-{profile}.tar.zst'
profile: ${{ inputs.profile }}

- name: Attach to the release
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
TAG: ${{ inputs.tag }}
ASSET: ${{ steps.stage.outputs.archive-path }}
OVERWRITE: ${{ inputs.overwrite }}
run: |
set -euo pipefail
test -s "$ASSET"
name=$(basename "$ASSET")

if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::no release exists for tag $TAG; create the release before publishing its baseline"
exit 1
fi

existing=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json assets --jq "[.assets[].name] | index(\"$name\") // empty")
if [ -n "$existing" ]; then
if [ "$OVERWRITE" != "true" ]; then
# Re-running publication for a release that already has its
# baseline is a no-op, not a silent replacement.
echo "::notice::$name is already published for $TAG; leaving the existing immutable asset in place"
exit 0
fi
# gh refuses an asset name that already exists unless --clobber
# is given, so the explicit-overwrite path has to pass it. It is
# deliberately confined to this branch: the default path above
# never replaces a published baseline.
echo "::warning::replacing the existing $name for $TAG at explicit request"
gh release upload "$TAG" "$ASSET" --repo "$GITHUB_REPOSITORY" --clobber
exit 0
fi

gh release upload "$TAG" "$ASSET" --repo "$GITHUB_REPOSITORY"
Loading
Loading