Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
222 changes: 222 additions & 0 deletions .ci-local/abicheck-diff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/bin/sh
# Advisory ABICheck scan. abi-diff.sh / ABICC remains authoritative.
set -eu

OLD_REF=${1:-}
NEW_REF=${2:-HEAD}
ABICHECK=${ABICHECK:-abicheck}
JOBS=${ABICHECK_MAKE_JOBS:-2}
REPORT_ROOT=${ABICHECK_REPORT_ROOT:-compat_reports/abicheck}
RUN_ROOT=${RUNNER_TEMP:-${TMPDIR:-/tmp}}/pvxs-abicheck-${GITHUB_RUN_ID:-$$}

explicit_old=1
new_sha=$(git rev-parse "$NEW_REF^{commit}")
if [ -z "$OLD_REF" ]; then
explicit_old=0
OLD_REF=$(git describe --tags --abbrev=0 "$new_sha")
fi
old_sha=$(git rev-parse "$OLD_REF^{commit}")
if [ "$explicit_old" -eq 0 ] && [ "$old_sha" = "$new_sha" ]; then
OLD_REF=$(git describe --tags --abbrev=0 "$new_sha^")
old_sha=$(git rev-parse "$OLD_REF^{commit}")
fi

mkdir -p "$RUN_ROOT" "$REPORT_ROOT"
export HOME="$RUN_ROOT/home"
export XDG_CACHE_HOME="$RUN_ROOT/cache"
export TMPDIR="$RUN_ROOT/tmp"
mkdir -p "$HOME" "$XDG_CACHE_HOME" "$TMPDIR"

EPICS_BASE=${EPICS_BASE:-}
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" ] && [ -d "$EPICS_BASE/include" ] || {
echo "EPICS_BASE include tree unavailable; use cue.py exec after prepare" >&2
exit 64
}
command -v bear >/dev/null || { echo "bear is required for complete build evidence" >&2; exit 64; }

OLD_SRC="$RUN_ROOT/old"
NEW_SRC="$RUN_ROOT/new"
# git archive has no enclosing directory; extract each revision into its own
# fixed root so paths and build evidence remain side-specific.
mkdir -p "$OLD_SRC" "$NEW_SRC"
git archive "$old_sha" | tar -C "$OLD_SRC" -xf -
git archive "$new_sha" | tar -C "$NEW_SRC" -xf -

prepare_build() {
src=$1
[ -f configure/RELEASE.local ] && cp configure/RELEASE.local "$src/configure/"
[ -f configure/CONFIG_SITE.local ] && cp configure/CONFIG_SITE.local "$src/configure/"
sed -i -e "s|\$(TOP)|$(pwd)|g" -e 's|-Werror||g' "$src"/configure/*.local 2>/dev/null || true
bear --output "$src/compile_commands.json" -- \
make -C "$src" CROSS_COMPILER_TARGET_ARCHS= OPT_CFLAGS='-g -Og' OPT_CXXFLAGS='-g -Og' ioc -j"$JOBS"
}

prepare_build "$OLD_SRC"
prepare_build "$NEW_SRC"

stage_headers() {
src=$1
target=$2
out=$3
public="$out/public"
support="$out/support"
if [ "$target" = libpvxs ]; then
source_headers="$src/src/pvxs"
generated_headers="$src/src/O.Common/pvxs"
[ -d "$source_headers" ] || { echo "missing PVXS public header root $source_headers" >&2; return 64; }
[ -f "$generated_headers/versionNum.h" ] || { echo "missing generated version header" >&2; return 64; }
headers=$(find "$source_headers" -maxdepth 1 -type f -name '*.h' -print | LC_ALL=C sort)
[ -n "$headers" ] || { echo "no PVXS public headers below $source_headers" >&2; return 64; }
mkdir -p "$public/pvxs"
while IFS= read -r header_src; do
cp "$header_src" "$public/pvxs/$(basename "$header_src")"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
done <<EOF
$headers
EOF
mkdir -p "$support/pvxs"
cp "$generated_headers/versionNum.h" "$support/pvxs/versionNum.h"
else
mkdir -p "$public/pvxs" "$support"
cp "$src/ioc/pvxs/iochooks.h" "$public/pvxs/iochooks.h"
fi
}

project_compile_db() {
src=$1
target=$2
out=$3
python3 - "$src/compile_commands.json" "$src" "$target" "$out" <<'PY'
import json, pathlib, sys
entries = json.load(open(sys.argv[1]))
source_root = pathlib.Path(sys.argv[2]).resolve()
target, out = sys.argv[3:]
component_root = (source_root / ('src' if target == 'libpvxs' else 'ioc')).resolve()
selected = []
for entry in entries:
source = pathlib.Path(entry['file'])
if not source.is_absolute():
source = pathlib.Path(entry.get('directory') or source_root) / source
try:
source.resolve().relative_to(component_root)
except ValueError:
continue
selected.append(entry)
if not selected:
raise SystemExit(f'no compile commands selected for {target}')
json.dump(selected, open(out, 'w'), indent=2)
PY
}

find_dso() {
matches=$(find "$1/lib" -type f -name "$2.so.*" -print | LC_ALL=C sort)
count=$(printf '%s\n' "$matches" | sed '/^$/d' | wc -l)
[ "$count" -eq 1 ] || {
echo "expected one $2 DSO below $1/lib, found $count" >&2
return 64
}
printf '%s\n' "$matches"
}

old_id=$(printf '%s' "$old_sha" | cut -c1-12)
new_id=$(printf '%s' "$new_sha" | cut -c1-12)
status_file="$REPORT_ROOT/summary.json"
printf '{"old_ref":"%s","old_sha":"%s","new_ref":"%s","new_sha":"%s","targets":[' \
"$OLD_REF" "$old_sha" "$NEW_REF" "$new_sha" > "$status_file"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
first=1
overall=0

run_one() {
target=$1
oldso=$(find_dso "$OLD_SRC" "$target")
newso=$(find_dso "$NEW_SRC" "$target")
[ -n "$oldso" ] && [ -n "$newso" ] || return 64
old_headers="$RUN_ROOT/headers-old-$target"
new_headers="$RUN_ROOT/headers-new-$target"
stage_headers "$OLD_SRC" "$target" "$old_headers" || return $?
stage_headers "$NEW_SRC" "$target" "$new_headers" || return $?
old_db="$OLD_SRC/compile_commands.$target.json"
new_db="$NEW_SRC/compile_commands.$target.json"
project_compile_db "$OLD_SRC" "$target" "$old_db" || return $?
project_compile_db "$NEW_SRC" "$target" "$new_db" || return $?
base="$RUN_ROOT/reports/${target}_${old_id}_to_${new_id}"
published="$REPORT_ROOT/${target}_${old_id}_to_${new_id}"
mkdir -p "$(dirname "$base")"
if "$ABICHECK" compare "$oldso" "$newso" \
--version "old=$old_sha" --version "new=$new_sha" \
--header "old=$old_headers/public" --header "new=$new_headers/public" \
--include "old:pvxs=$old_headers/public" --include "new:pvxs=$new_headers/public" \
--include "old:pvxs-config=$old_headers/support" --include "new:pvxs-config=$new_headers/support" \
--include "old:pvxs-core=$RUN_ROOT/headers-old-libpvxs/public" --include "new:pvxs-core=$RUN_ROOT/headers-new-libpvxs/public" \
--include "old:pvxs-core-config=$RUN_ROOT/headers-old-libpvxs/support" --include "new:pvxs-core-config=$RUN_ROOT/headers-new-libpvxs/support" \
--include "old:epics=$EPICS_BASE/include" --include "new:epics=$EPICS_BASE/include" \
--include "old:epics-os=$EPICS_BASE/include/os/Linux" --include "new:epics-os=$EPICS_BASE/include/os/Linux" \
--include "old:epics-gcc=$EPICS_BASE/include/compiler/gcc" --include "new:epics-gcc=$EPICS_BASE/include/compiler/gcc" \
--depth source --sources "old=$OLD_SRC" --sources "new=$NEW_SRC" \
--build-info "old=$old_db" --build-info "new=$new_db" \
--config "$PWD/.ci-local/abicheck.yml" \
--format review --write "json=$base.json" -o "$base.md"
then
rc=0
else
rc=$?
fi
if [ ! -s "$base.json" ] || [ ! -s "$base.md" ]; then
echo "missing comparison reports for $target" >&2
rc=64
elif ! python3 - "$base.json" <<'PY'
import json, sys
try:
report = json.load(open(sys.argv[1]))
except (OSError, json.JSONDecodeError) as exc:
raise SystemExit(f"invalid JSON report: {exc}")
if not isinstance(report, dict) or not isinstance(report.get("verdict"), str):
raise SystemExit("missing comparison verdict")
assurance = report.get("analysis_assurance")
if not isinstance(assurance, dict) or assurance.get("status") != "complete":
raise SystemExit("analysis assurance is not complete")
if report.get("analysis_assurance_exit_contribution") != 0:
raise SystemExit("analysis assurance gate is inconsistent")
PY
then
echo "invalid or incomplete analysis assurance for $target" >&2
rc=1
else
cp "$base.json" "$published.json"
cp "$base.md" "$published.md"
: > "$RUN_ROOT/$target.report-ready"
fi
printf '%s\n' "$rc" > "$RUN_ROOT/$target.exit-code"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && [ -f "$base.md" ]; then cat "$base.md" >> "$GITHUB_STEP_SUMMARY"; fi
case "$rc" in 0|2|4) return 0;; *) return "$rc";; esac
}

append_target() {
target=$1
rc=$2
base="$REPORT_ROOT/${target}_${old_id}_to_${new_id}"
[ "$first" -eq 1 ] || printf ',' >> "$status_file"
first=0
if [ -f "$RUN_ROOT/$target.report-ready" ] && [ -f "$base.json" ]; then
printf '{"target":"%s","exit_code":%s,"report":"%s.json"}' "$target" "$rc" "$(basename "$base")" >> "$status_file"
else
printf '{"target":"%s","exit_code":%s,"report":null}' "$target" "$rc" >> "$status_file"
fi
}

for target in libpvxs libpvxsIoc; do
if run_one "$target"; then
rc=0
else
rc=$?
fi
if [ -f "$RUN_ROOT/$target.exit-code" ]; then
rc=$(cat "$RUN_ROOT/$target.exit-code")
fi
append_target "$target" "$rc"
case "$rc" in 0|2|4) ;; *) overall=1;; esac
done
printf '],"integration_health":%s}\n' "$overall" >> "$status_file"
exit "$overall"
8 changes: 8 additions & 0 deletions .ci-local/abicheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# CastXML packaged on the GitHub runner cannot parse libstdc++ with PVXS's
# default C++11 mode. This is deliberately recorded as extraction context;
# the libraries themselves keep PVXS's normal build flags.
compile:
options:
- -std=c++17
assurance:
require_complete: true
78 changes: 78 additions & 0 deletions .github/workflows/abicheck-shadow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: ABICheck shadow ABI scan

on:
pull_request:
push:
branches: [master]
tags: ['*']
workflow_dispatch:

concurrency:
group: abicheck-shadow-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# The scanner is an advisory, parallel signal. It needs no PR comments, no
# status write access, and no repository secrets.
permissions:
contents: read

jobs:
abicheck-shadow:
runs-on: ubuntu-latest
# A full source-evidence scan runs two revisions of both public DSOs.
# Keep this above the measured ~100-minute worst case, not at 45 minutes.
timeout-minutes: 120
env:
SETUP_PATH: .ci-local
SET: defaults
CMP: gcc
BCFG: default
BASE: "7.0"
_PVXS_ABORT_ON_CRIT: 1
PVXS_LOG: pvxs.*=WARN
ABICHECK_MAKE_JOBS: "2"
ABICHECK_REF: 0ab7da2eba3016bcad86657fb74239c66ef3ffeb

steps:
- uses: actions/checkout@v6
with:
fetch-depth: "0"
submodules: true
persist-credentials: false

- name: Install scanner tools
shell: bash
run: |
set -euxo pipefail
sudo apt-get update
sudo apt-get -y install libreadline-dev libevent-dev cmake bear
/usr/share/miniconda/bin/conda install -y -c conda-forge castxml
VENV="$RUNNER_TEMP/abicheck-venv"
python -m venv "$VENV"
"$VENV/bin/pip" install --disable-pip-version-check \
"git+https://github.com/abicheck/abicheck@${ABICHECK_REF}"
echo "/usr/share/miniconda/bin" >> "$GITHUB_PATH"
echo "$VENV/bin" >> "$GITHUB_PATH"
echo "ABICHECK=$VENV/bin/abicheck" >> "$GITHUB_ENV"

- name: Verify scanner tools
shell: bash
run: |
set -euxo pipefail
castxml --version
"$ABICHECK" --version

- name: Prepare EPICS dependencies
run: python .ci/cue.py prepare

- name: Run full source-evidence shadow scan
run: python .ci/cue.py exec ./.ci-local/abicheck-diff.sh

- name: Upload ABICheck reports
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
retention-days: 30
name: abicheck-shadow
path: compat_reports/abicheck
if-no-files-found: error
Loading