Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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"
]
}
]
25 changes: 25 additions & 0 deletions .ci-local/abicheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Extraction context for the advisory abicheck shadow check.
#
# This describes how the PUBLIC HEADERS are parsed. 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:
# CURRENT LIMITATION. PVXS's supported consumer language mode is C++11,
# but abicheck's extraction pipeline cannot parse these headers against
# libstdc++ 13 in C++11 or C++14 (each fails, for a different reason, on
# the toolchain CI selects). C++17 extraction is therefore a disclosed
# deviation from the consumer language mode -- it is NOT validation of
# C++11 consumer behaviour -- and it is applied identically to both
# components and to both sides of every comparison. Remove it once the
# pipeline parses the headers in C++11. Measurements and the upstream
# issue are linked from the pull request.
- -std=c++17
# Deliberately NOT set: 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 is reported as incomplete, never as
# a clean result. The shadow gate stays advisory; this controls what the
# analysis may claim, not whether CI goes red.
require_complete: true
143 changes: 143 additions & 0 deletions .github/actions/abicheck-capture/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: 'PVXS ABI capture'
description: >-
Capture ABI/API evidence for libpvxs and libpvxsIoc from an installation 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. Selecting the real shared object
out of its SONAME alias chain, ELF/machine agreement, expanding the owned
header sets, refusing a stale exclusion and binding the two build-decided
values into the declaration all belong to abicheck's baseline Action,
which reads .ci-local/abicheck-components.json.

Shared by ci-scripts-build.yml's candidate capture and
abicheck-baseline.yml's historical bootstrap, so the two cannot drift.

inputs:
top:
description: >
Root of the built PVXS tree to capture from. Defaults to the
workspace; the historical bootstrap points this at its own checkout.
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:
- name: Resolve EPICS build context
id: epics
shell: bash
env:
TOP: ${{ inputs.top || github.workspace }}
SPEC_IN: ${{ inputs.component-spec }}
CFG_IN: ${{ inputs.build-config }}
WORKSPACE: ${{ github.workspace }}
run: |
set -eu

# Both trusted inputs belong to the CHECKOUT, not to the tree being
# captured: the bootstrap points `top` at a revision that predates
# them, so a relative path resolved after `cd "$TOP"` would read a
# missing or stale file from that revision.
case "$SPEC_IN" in /*) spec=$SPEC_IN ;; *) spec=$WORKSPACE/$SPEC_IN ;; esac
case "$CFG_IN" in '') cfg= ;; /*) cfg=$CFG_IN ;; *) cfg=$WORKSPACE/$CFG_IN ;; esac
[ -f "$spec" ] || { echo "::error::component declaration $spec does not exist"; exit 64; }
if [ -n "$cfg" ] && [ ! -f "$cfg" ]; then
echo "::error::extraction config $cfg does not exist"; exit 64
fi

cd "$TOP"

# 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
# This capture is declared for a native Linux install layout only.
case "$EPICS_HOST_ARCH" in
linux-*) ;;
*) echo "::error::expected a native Linux host arch, got '${EPICS_HOST_ARCH:-<unset>}'"
exit 64 ;;
esac

{
echo "epics-base=$EPICS_BASE"
echo "host-arch=$EPICS_HOST_ARCH"
echo "spec=$spec"
echo "build-config=$cfg"
} >> "$GITHUB_OUTPUT"

- name: Capture ABI snapshots (libpvxs, libpvxsIoc)
id: capture
uses: abicheck/abicheck/actions/baseline@902ee996795dbe529a5d6d348a220864634c0e2b
with:
library-spec: ${{ steps.epics.outputs.spec }}
# Binding is abicheck's: it walks the document as JSON and inserts
# each value literally, so a path containing `&`, a quote or a
# backslash is not mangled the way a textual pass would. The two
# names below are the whole allowlist; any other ${...} is refused.
library-spec-bindings: |
EPICS_BASE=${{ steps.epics.outputs.epics-base }}
EPICS_HOST_ARCH=${{ steps.epics.outputs.host-arch }}
library-root: ${{ inputs.top || github.workspace }}
output-dir: ${{ inputs.output-dir }}
project-ref: ${{ inputs.project-ref }}
profile: ${{ inputs.profile }}
depth: headers
build-config: ${{ steps.epics.outputs.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
196 changes: 196 additions & 0 deletions .github/workflows/abicheck-baseline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Publish the release-contract ABI baseline-set for the selected profile.
#
# Two paths, both running only from this repository's default branch, and
# both publishing through abicheck's own reusable publish-baseline.yml --
# which owns packaging, the manifest/schema/profile/generation/digest gate,
# tag resolution and the immutability chain (identity-verified idempotent
# republish, fail-closed on a conflicting asset).
#
# automatic A tag build of "PVXS EPICS" already captured the tagged
# revision. Its capture is published from that run. Nothing
# is rebuilt.
#
# bootstrap A historical release predates the capture integration, so no
# tag build of it ever produced a baseline-set, and dispatching
# the old workflow cannot help: the workflow AT that tag has no
# capture step. This path builds the requested revision once,
# here, with the current trusted tooling. One-time per release,
# never part of a pull request.
#
# The accepted-main channel needs nothing here: a pull request resolves it
# from the successful default-branch run for its own base commit.

name: ABI baseline

on:
workflow_run:
workflows: ["PVXS EPICS"]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build, capture and publish a baseline-set for.'
required: true
type: string

permissions:
contents: read

env:
ABICHECK_PROFILE: linux-x86_64-gcc-default-base7.0-bundled-libevent
ABICHECK_REF: 902ee996795dbe529a5d6d348a220864634c0e2b

jobs:
# ── Automatic: publish the tag build's own capture ─────────────────────
#
# A push run of "PVXS EPICS" can be a branch push as easily as a tag push,
# so ask the shared verifier whether the ref really is a tag naming the
# commit that was built, and publish only then. PVXS tags are bare
# versions ("1.5.2"); the verifier assumes no "v" prefix, resolves
# refs/tags/<name> explicitly rather than through a revision endpoint that
# would resolve a branch, and peels an annotated tag.
tag-gate:
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
eligible: ${{ steps.tag.outputs.eligible }}
steps:
- id: tag
uses: abicheck/abicheck/actions/verify-baseline-source@902ee996795dbe529a5d6d348a220864634c0e2b
with:
mode: tag
tag: ${{ github.event.workflow_run.head_branch }}
built-sha: ${{ github.event.workflow_run.head_sha }}
- if: ${{ steps.tag.outputs.eligible != 'true' }}
env:
OUTCOME: ${{ steps.tag.outputs.outcome }}
run: |
echo "nothing to publish from this run; tag outcome: $OUTCOME"

publish:
needs: tag-gate
if: ${{ needs.tag-gate.outputs.eligible == 'true' }}
permissions:
# actions: read is what takes the artifact bytes through the API from
# the producer run this publication was triggered by.
actions: read
contents: write
uses: abicheck/abicheck/.github/workflows/publish-baseline.yml@902ee996795dbe529a5d6d348a220864634c0e2b
with:
# Publish the finished set the producer captured; no dump, no build
# query, no compiler, no comparison happens in this workflow.
baseline-set-artifact-prefix: abicheck-candidate-
# The set comes from a DIFFERENT, already-completed run, so its origin
# is established rather than assumed: repository, workflow identity,
# event, id, attempt and conclusion are each checked, the artifacts are
# then fetched by their own ids, and a pull-request-triggered producer
# is refused unconditionally.
baseline-set-source-run-id: ${{ github.event.workflow_run.id }}
baseline-set-source-repository: ${{ github.repository }}
baseline-set-source-run-attempt: ${{ github.event.workflow_run.run_attempt }}
baseline-set-expect-workflow: .github/workflows/ci-scripts-build.yml
baseline-set-expect-event: push
baseline-set-allowed-conclusions: success
release-tag: ${{ github.event.workflow_run.head_branch }}
# The publication tag and the revision the set records are two
# different identifiers: a producer stamps the commit it built.
# 'commit' resolves the tag through refs/tags/<name> and requires the
# set's own project_ref to equal that commit -- never the other way
# round, and never a tag-valued rewrite of a SHA-valued manifest.
expected-project-ref: commit
baseline-generation: '1'
asset-name-template: 'abicheck-baseline-{profile}.tar.zst'

# ── Bootstrap: build a historical release once, here ───────────────────
#
# Split in two on purpose. This job runs the requested revision's own code
# (cue.py, its submodules, its makefiles) and therefore holds no write
# scope. It hands the captured set to the publishing job as an artifact.
bootstrap-build:
if: ${{ github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
# The trusted tooling (capture action, component declaration) comes from
# this checkout; the revision being captured is checked out separately
# under historical/, and its own workflow is never run.
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Resolve the requested tag
id: tag
uses: abicheck/abicheck/actions/verify-baseline-source@902ee996795dbe529a5d6d348a220864634c0e2b
with:
mode: tag
tag: ${{ inputs.tag }}

- name: Check out the historical revision
uses: actions/checkout@v6
with:
ref: ${{ steps.tag.outputs.commit-sha }}
submodules: true
persist-credentials: false
path: historical

- name: apt-get install
run: |
sudo apt-get update
sudo apt-get -y install libreadline-dev cmake

# PVXS's normal build commands, the same ones the CI matrix runs.
- name: Build the historical revision
working-directory: historical
run: |
set -eu
python .ci/cue.py prepare
python .ci/cue.py exec python .ci-local/libevent.py
python .ci/cue.py build

# Same declaration and same shared action as the matrix leg, so the
# bootstrap cannot drift from normal capture.
- name: Capture the historical revision
uses: ./.github/actions/abicheck-capture
with:
top: ${{ github.workspace }}/historical
output-dir: ${{ runner.temp }}/baseline-set
project-ref: ${{ steps.tag.outputs.commit-sha }}
profile: ${{ env.ABICHECK_PROFILE }}
abicheck-ref: ${{ env.ABICHECK_REF }}

# An artifact, not a path: the set has to cross a real job boundary, and
# a producer-local directory does not exist in the publishing job.
- name: Hand the baseline-set to the publishing job
uses: actions/upload-artifact@v7
with:
name: abicheck-bootstrap-${{ env.ABICHECK_PROFILE }}
path: ${{ runner.temp }}/baseline-set
if-no-files-found: error
retention-days: 1

bootstrap-publish:
needs: bootstrap-build
# workflow_dispatch runs from whatever ref was selected. Refuse to
# publish off anything but the default branch, so the write-capable half
# can never be a dispatched branch's copy of this file.
if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
permissions:
actions: read
contents: write
uses: abicheck/abicheck/.github/workflows/publish-baseline.yml@902ee996795dbe529a5d6d348a220864634c0e2b
with:
# Same-run handoff: the artifact was uploaded by bootstrap-build, in
# this run, so no source-run verification is needed or possible.
baseline-set-artifact-prefix: abicheck-bootstrap-
release-tag: ${{ inputs.tag }}
expected-project-ref: commit
baseline-generation: '1'
asset-name-template: 'abicheck-baseline-{profile}.tar.zst'
Loading