Repository navigation
ci: reuse the normal build for an advisory ABI/API check, published safely #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
napetrov
wants to merge
46
commits into
master
Choose a base branch
from
abicheck-shadow-integration-clean
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 37 commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
8bf1823
ci: add ABICheck shadow ABI scan
napetrov 0b98079
ci: require comparable ABICheck shadow evidence
napetrov 24702cc
ci: install CastXML from conda-forge
napetrov 3201533
ci: collect target-specific ABICheck evidence
napetrov 60f7603
ci: retain ABICheck target failures
napetrov d95ba93
ci: record pvxs C++ dialect in scan evidence
napetrov 8fe7884
ci: override CastXML dialect for GCC 13
napetrov 2a26a19
ci: allow complete pvxs shadow scans
napetrov 839b511
ci: stage PVXS public headers for shadow ABI scan
napetrov d2d4c61
ci: harden PVXS ABICheck shadow integration
napetrov 3825f0d
ci: fail closed on invalid ABI evidence
napetrov d0b31ba
ci: separate public and generated PVXS headers
napetrov c53cc09
ci: stage declared PVXS header sources
napetrov b8a557d
ci: escape ABI summary references
napetrov f4f576b
ci: fail closed when staging ABI reports
napetrov c4de3ae
ci: use L2 ABI scan in shadow workflow
napetrov e955819
ci: update ABICheck scanner to latest main
napetrov 07c17aa
ci: use current ABICheck export syntax
napetrov d1023b8
ci: refresh ABICheck main pin
napetrov a5c52a6
ci: correct ABICheck main revision pin
napetrov 6b096a1
ci: reuse the normal build for the ABI check and publish it safely
napetrov f1cc10a
ci: read the snapshot path from the manifest's own snapshot field
napetrov 8ffffa8
ci: fix baseline eligibility, report identity and the incomplete cont…
napetrov 3732bc9
ci: drop the redundant ABI capture failure marker
napetrov 2f9c269
ci: make the explicit baseline overwrite path actually work
napetrov 0beca2b
ci: keep per-target detail reachable, and pin the reviewed publisher
napetrov 51b33f0
ci: repair the workflow file broken by an invalid env context
napetrov 440fa8e
ci: order the sticky comment by the producer run, and refresh the pin
napetrov 65d3f8d
ci: pin the whole integration to the merged abicheck revision
claude d2c3904
ci: hand the generic ABI machinery back to abicheck
claude 22ca9d5
Merge remote-tracking branch 'origin/master' into abicheck-shadow-int…
claude 7143f5d
docs: record the first real-runner validation of the migrated Actions
claude b13f69d
ci: fix four defects the review found in the migrated integration
claude d97e677
ci: repin to pick up the report fix that made the ordering guard work
claude a4965ce
docs: record why the upstream baseline publisher does not fit yet
claude ea4f758
ci: enforce the default-branch restriction the bootstrap only claimed
claude e27eb5f
docs: state that no real baseline comparison has run on this branch
claude 08b895b
ci: verify a baseline-set's revision before publishing it under a tag
claude 5a129fa
ci: bind the spec as JSON, verify the analysed commit, and say what ran
claude f5ade4a
ci: retest extraction on the CI toolchain, and actually compare somet…
claude 40c8eb8
ci: drop a renderer step that cannot load, and record why
claude bc8e0a1
Merge master into abicheck integration branch
napetrov e4c5428
ci: consolidate the abicheck integration onto supported owners
napetrov daffed4
ci: declare the expected checks independently of comparison execution
napetrov 7cbd1c3
ci: stop pre-creating the baseline channel directories
napetrov d4023bf
ci: repin abicheck to current main and drop default-restating inputs
napetrov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| ] | ||
| } | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
|
|
||
| # 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.