From 34a9001ccf2710a2a41814f443c819613c081009 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Fri, 30 Jan 2026 20:36:02 -0600 Subject: [PATCH 1/4] Add GitHub Actions for C ABI checking - Add check-c-abi.yaml workflow for PRs - Add store-c-abi-baseline.yaml workflow for releases - Integrate ABI check into pr.yaml pipeline Co-authored-by: Cursor --- .github/workflows/check-c-abi.yaml | 137 ++++++++++++++++++++ .github/workflows/pr.yaml | 4 + .github/workflows/store-c-abi-baseline.yaml | 105 +++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 .github/workflows/check-c-abi.yaml create mode 100644 .github/workflows/store-c-abi-baseline.yaml diff --git a/.github/workflows/check-c-abi.yaml b/.github/workflows/check-c-abi.yaml new file mode 100644 index 0000000000..f8cb74179d --- /dev/null +++ b/.github/workflows/check-c-abi.yaml @@ -0,0 +1,137 @@ +name: C ABI Compatibility Check + +on: + workflow_call: + pull_request: + paths: + - 'c/include/**' + - 'ci/check_c_abi.py' + - '.github/workflows/check-c-abi.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-c-abi: + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libclang-dev \ + clang \ + cmake \ + ninja-build + + - name: Install Python dependencies + run: | + pip install --upgrade pip + pip install msgspec libclang termcolor + + - name: Build C++ project to get dependencies (dlpack) + run: | + mkdir -p cpp/build + cd cpp/build + cmake .. \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + # We only need the configure step to fetch dlpack headers + # No need to actually build everything + echo "Build directory created and dependencies fetched" + + - name: Download baseline ABI from artifact store + id: download-baseline + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: c-abi-baseline + path: baseline/ + + - name: Extract baseline ABI from main branch (if artifact not found) + if: steps.download-baseline.outcome == 'failure' + run: | + echo "Baseline ABI artifact not found, extracting from main branch..." + + # Checkout main branch to a separate directory + git worktree add ../cuvs-main main + + # Build main branch to get dlpack headers + mkdir -p ../cuvs-main/cpp/build + cd ../cuvs-main/cpp/build + cmake .. \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cd ../../.. + + # Extract baseline ABI from main branch + mkdir -p baseline + python ci/check_c_abi.py extract \ + --header_path ../cuvs-main/c/include \ + --include_file cuvs/core/all.h \ + --output_file baseline/c_abi.json.gz + + echo "Baseline ABI extracted from main branch" + + - name: Analyze current branch for ABI breaking changes + run: | + python ci/check_c_abi.py analyze \ + --abi_file baseline/c_abi.json.gz \ + --header_path c/include \ + --include_file cuvs/core/all.h + + - name: Comment on PR with results + if: failure() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## ⚠️ C ABI Breaking Changes Detected + +This PR introduces breaking changes to the C ABI. Please review the changes carefully. + +Breaking ABI changes are only allowed in major releases. If this is intentional for a major release, +the baseline ABI will need to be updated after merge. + +See the job logs for details on what specific changes were detected. + +### What are breaking ABI changes? + +Breaking ABI changes include: +- Removing functions from the public API +- Changing function signatures (parameters or return types) +- Removing struct members or changing their types +- Removing or changing enum values + +### Next steps + +1. Review the changes flagged in the CI logs +2. If these changes are unintentional, update your PR to maintain ABI compatibility +3. If these changes are required, ensure: + - This is part of a major version release + - The changes are documented in the changelog + - Migration guide is provided for users + +For more information, see the [C ABI documentation](../docs/source/c_developer_guide.md). +` + }); diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 1dd02968e2..9affaf12e1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -12,6 +12,7 @@ jobs: - check-nightly-ci - changed-files - checks + - check-c-abi - conda-cpp-build - conda-cpp-tests - conda-cpp-checks @@ -136,6 +137,9 @@ jobs: with: enable_check_generated_files: false ignored_pr_jobs: "telemetry-summarize" + check-c-abi: + needs: telemetry-setup + uses: ./.github/workflows/check-c-abi.yaml conda-cpp-build: needs: checks secrets: inherit diff --git a/.github/workflows/store-c-abi-baseline.yaml b/.github/workflows/store-c-abi-baseline.yaml new file mode 100644 index 0000000000..7f8c218e75 --- /dev/null +++ b/.github/workflows/store-c-abi-baseline.yaml @@ -0,0 +1,105 @@ +name: Store C ABI Baseline + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version tag to store baseline for (e.g., v26.02.00)' + required: true + type: string + +jobs: + store-baseline: + runs-on: ubuntu-latest + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || inputs.version }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libclang-dev \ + clang \ + cmake \ + ninja-build + + - name: Install Python dependencies + run: | + pip install --upgrade pip + pip install msgspec libclang + + - name: Build C++ project to get dependencies (dlpack) + run: | + mkdir -p cpp/build + cd cpp/build + cmake .. \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + + - name: Extract C ABI baseline + run: | + mkdir -p abi-baselines + python ci/check_c_abi.py extract \ + --header_path c/include \ + --include_file cuvs/core/all.h \ + --output_file abi-baselines/c_abi_${{ github.event.release.tag_name || inputs.version }}.json.gz + + - name: Upload ABI baseline as artifact + uses: actions/upload-artifact@v4 + with: + name: c-abi-baseline-${{ github.event.release.tag_name || inputs.version }} + path: abi-baselines/c_abi_*.json.gz + retention-days: 400 # ~13 months, enough for patch releases + + - name: Commit baseline to repository (for long-term storage) + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create a baselines branch if it doesn't exist + git fetch origin baselines:baselines || git checkout -b baselines + git checkout baselines + + # Copy the baseline file + mkdir -p c-abi-baselines + cp abi-baselines/c_abi_*.json.gz c-abi-baselines/ + + # Commit and push + git add c-abi-baselines/ + git commit -m "Add C ABI baseline for ${{ github.event.release.tag_name || inputs.version }}" + git push origin baselines + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create release comment + if: github.event_name == 'release' + uses: actions/github-script@v7 + with: + script: | + const tagName = context.payload.release.tag_name; + + github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.payload.release.target_commitish, + body: `✅ C ABI baseline stored for release ${tagName} + +This baseline will be used to check for breaking ABI changes in future PRs. + +The baseline is stored in: +- Artifact: \`c-abi-baseline-${tagName}\` (available for ~13 months) +- Branch: \`baselines\` (permanent storage) +` + }); From 53a431fe7e65d2129c90ce487a1f4c9473ec91c9 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Fri, 30 Jan 2026 21:11:06 -0600 Subject: [PATCH 2/4] Store ABI baseline on every merge to main - Extract and store main baseline on push to main (never expires) - PRs download and compare against main baseline - Release workflow archives the main baseline with version tag - Eliminates duplicate ABI extraction work Co-authored-by: Cursor --- .github/workflows/check-c-abi.yaml | 87 +++++++++++++++++---- .github/workflows/store-c-abi-baseline.yaml | 71 +++++------------ 2 files changed, 90 insertions(+), 68 deletions(-) diff --git a/.github/workflows/check-c-abi.yaml b/.github/workflows/check-c-abi.yaml index f8cb74179d..a158fea952 100644 --- a/.github/workflows/check-c-abi.yaml +++ b/.github/workflows/check-c-abi.yaml @@ -7,13 +7,75 @@ on: - 'c/include/**' - 'ci/check_c_abi.py' - '.github/workflows/check-c-abi.yaml' + push: + branches: + - main + paths: + - 'c/include/**' + - 'ci/check_c_abi.py' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - check-c-abi: + # Extract and store baseline ABI from main branch + update-baseline: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libclang-dev \ + clang \ + cmake \ + ninja-build + + - name: Install Python dependencies + run: | + pip install --upgrade pip + pip install msgspec libclang termcolor + + - name: Build C++ project to get dependencies (dlpack) + run: | + mkdir -p cpp/build + cd cpp/build + cmake .. \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + echo "Build directory created and dependencies fetched" + + - name: Extract ABI from main branch + run: | + mkdir -p baseline + python ci/check_c_abi.py extract \ + --header_path c/include \ + --include_file cuvs/core/all.h \ + --output_file baseline/c_abi.json.gz + echo "ABI extracted from main branch" + + - name: Store main baseline (never expires) + uses: actions/upload-artifact@v4 + with: + name: c-abi-baseline-main + path: baseline/c_abi.json.gz + retention-days: 0 # Never expire + + # Check PRs for breaking ABI changes + check-pr: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_call' runs-on: ubuntu-latest steps: - name: Checkout PR branch @@ -49,27 +111,23 @@ jobs: -DCMAKE_BUILD_TYPE=Debug \ -DBUILD_TESTS=OFF \ -DBUILD_EXAMPLES=OFF - # We only need the configure step to fetch dlpack headers - # No need to actually build everything echo "Build directory created and dependencies fetched" - - name: Download baseline ABI from artifact store + - name: Download baseline ABI from main id: download-baseline continue-on-error: true - uses: actions/download-artifact@v4 + uses: dawidd6/action-download-artifact@v3 with: - name: c-abi-baseline + name: c-abi-baseline-main + workflow: check-c-abi.yaml + branch: main path: baseline/ - - name: Extract baseline ABI from main branch (if artifact not found) + - name: Extract baseline ABI from main branch (fallback) if: steps.download-baseline.outcome == 'failure' run: | echo "Baseline ABI artifact not found, extracting from main branch..." - - # Checkout main branch to a separate directory git worktree add ../cuvs-main main - - # Build main branch to get dlpack headers mkdir -p ../cuvs-main/cpp/build cd ../cuvs-main/cpp/build cmake .. \ @@ -78,14 +136,11 @@ jobs: -DBUILD_TESTS=OFF \ -DBUILD_EXAMPLES=OFF cd ../../.. - - # Extract baseline ABI from main branch mkdir -p baseline python ci/check_c_abi.py extract \ --header_path ../cuvs-main/c/include \ --include_file cuvs/core/all.h \ --output_file baseline/c_abi.json.gz - echo "Baseline ABI extracted from main branch" - name: Analyze current branch for ABI breaking changes @@ -96,12 +151,10 @@ jobs: --include_file cuvs/core/all.h - name: Comment on PR with results - if: failure() + if: failure() && github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, diff --git a/.github/workflows/store-c-abi-baseline.yaml b/.github/workflows/store-c-abi-baseline.yaml index 7f8c218e75..f93ec49b6e 100644 --- a/.github/workflows/store-c-abi-baseline.yaml +++ b/.github/workflows/store-c-abi-baseline.yaml @@ -1,4 +1,4 @@ -name: Store C ABI Baseline +name: Archive C ABI Baseline for Release on: release: @@ -6,62 +6,31 @@ on: workflow_dispatch: inputs: version: - description: 'Version tag to store baseline for (e.g., v26.02.00)' + description: 'Version tag to archive baseline for (e.g., v26.02.00)' required: true type: string jobs: - store-baseline: + archive-baseline: runs-on: ubuntu-latest steps: - - name: Checkout release tag + - name: Checkout repository uses: actions/checkout@v4 - with: - ref: ${{ github.event.release.tag_name || inputs.version }} - - name: Set up Python - uses: actions/setup-python@v5 + - name: Download main baseline artifact + uses: dawidd6/action-download-artifact@v3 with: - python-version: '3.11' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libclang-dev \ - clang \ - cmake \ - ninja-build - - - name: Install Python dependencies - run: | - pip install --upgrade pip - pip install msgspec libclang - - - name: Build C++ project to get dependencies (dlpack) - run: | - mkdir -p cpp/build - cd cpp/build - cmake .. \ - -GNinja \ - -DCMAKE_BUILD_TYPE=Debug \ - -DBUILD_TESTS=OFF \ - -DBUILD_EXAMPLES=OFF - - - name: Extract C ABI baseline - run: | - mkdir -p abi-baselines - python ci/check_c_abi.py extract \ - --header_path c/include \ - --include_file cuvs/core/all.h \ - --output_file abi-baselines/c_abi_${{ github.event.release.tag_name || inputs.version }}.json.gz + name: c-abi-baseline-main + workflow: check-c-abi.yaml + branch: main + path: baseline/ - - name: Upload ABI baseline as artifact + - name: Archive baseline with release version uses: actions/upload-artifact@v4 with: name: c-abi-baseline-${{ github.event.release.tag_name || inputs.version }} - path: abi-baselines/c_abi_*.json.gz - retention-days: 400 # ~13 months, enough for patch releases + path: baseline/c_abi.json.gz + retention-days: 400 # ~13 months - name: Commit baseline to repository (for long-term storage) run: | @@ -69,16 +38,16 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" # Create a baselines branch if it doesn't exist - git fetch origin baselines:baselines || git checkout -b baselines - git checkout baselines + git fetch origin baselines:baselines 2>/dev/null || git checkout --orphan baselines + git checkout baselines 2>/dev/null || true - # Copy the baseline file + # Copy the baseline file with version name mkdir -p c-abi-baselines - cp abi-baselines/c_abi_*.json.gz c-abi-baselines/ + cp baseline/c_abi.json.gz c-abi-baselines/c_abi_${{ github.event.release.tag_name || inputs.version }}.json.gz # Commit and push git add c-abi-baselines/ - git commit -m "Add C ABI baseline for ${{ github.event.release.tag_name || inputs.version }}" + git commit -m "Archive C ABI baseline for ${{ github.event.release.tag_name || inputs.version }}" git push origin baselines env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -94,9 +63,9 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, commit_sha: context.payload.release.target_commitish, - body: `✅ C ABI baseline stored for release ${tagName} + body: `✅ C ABI baseline archived for release ${tagName} -This baseline will be used to check for breaking ABI changes in future PRs. +This baseline has been archived from the main branch and will be available for historical reference. The baseline is stored in: - Artifact: \`c-abi-baseline-${tagName}\` (available for ~13 months) From 2ad7c0ffc4a18e331bc23203ab01256321009671 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Fri, 30 Jan 2026 21:17:52 -0600 Subject: [PATCH 3/4] Add workflow_dispatch to allow manual baseline creation Enables bootstrapping the initial c-abi-baseline-main artifact Co-authored-by: Cursor --- .github/workflows/check-c-abi.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-c-abi.yaml b/.github/workflows/check-c-abi.yaml index a158fea952..cc07bbd092 100644 --- a/.github/workflows/check-c-abi.yaml +++ b/.github/workflows/check-c-abi.yaml @@ -1,6 +1,7 @@ name: C ABI Compatibility Check on: + workflow_dispatch: # Allow manual trigger for bootstrap workflow_call: pull_request: paths: @@ -21,7 +22,7 @@ concurrency: jobs: # Extract and store baseline ABI from main branch update-baseline: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') runs-on: ubuntu-latest steps: - name: Checkout main branch From 366f33984662495a04a3f4dff57ac28ecdfcaded Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Fri, 30 Jan 2026 21:32:25 -0600 Subject: [PATCH 4/4] Implement commit-specific baselines with cascade fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Store baselines with commit SHA for precise comparisons - Cascade: try merge-base → latest main → extract fresh - Add concurrency control to prevent race conditions - Report which baseline source was used for transparency Co-authored-by: Cursor --- .github/workflows/check-c-abi.yaml | 56 +++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-c-abi.yaml b/.github/workflows/check-c-abi.yaml index cc07bbd092..6c3461a57a 100644 --- a/.github/workflows/check-c-abi.yaml +++ b/.github/workflows/check-c-abi.yaml @@ -24,6 +24,9 @@ jobs: update-baseline: if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') runs-on: ubuntu-latest + concurrency: + group: abi-baseline-update + cancel-in-progress: false # Queue updates, don't skip steps: - name: Checkout main branch uses: actions/checkout@v4 @@ -65,9 +68,16 @@ jobs: --header_path c/include \ --include_file cuvs/core/all.h \ --output_file baseline/c_abi.json.gz - echo "ABI extracted from main branch" + echo "ABI extracted from main branch (commit: ${{ github.sha }})" - - name: Store main baseline (never expires) + - name: Store commit-specific baseline + uses: actions/upload-artifact@v4 + with: + name: c-abi-baseline-${{ github.sha }} + path: baseline/c_abi.json.gz + retention-days: 90 # Keep for 3 months + + - name: Store main baseline (latest, never expires) uses: actions/upload-artifact@v4 with: name: c-abi-baseline-main @@ -114,8 +124,27 @@ jobs: -DBUILD_EXAMPLES=OFF echo "Build directory created and dependencies fetched" - - name: Download baseline ABI from main - id: download-baseline + - name: Find merge base commit + id: merge-base + run: | + git fetch origin main + MERGE_BASE=$(git merge-base HEAD origin/main) + echo "merge_base_sha=${MERGE_BASE}" >> $GITHUB_OUTPUT + echo "Merge base commit: ${MERGE_BASE}" + + - name: Try to download baseline for merge-base commit (most accurate) + id: download-merge-base + continue-on-error: true + uses: dawidd6/action-download-artifact@v3 + with: + name: c-abi-baseline-${{ steps.merge-base.outputs.merge_base_sha }} + workflow: check-c-abi.yaml + commit: ${{ steps.merge-base.outputs.merge_base_sha }} + path: baseline/ + + - name: Try to download latest main baseline (fallback 1) + id: download-main + if: steps.download-merge-base.outcome == 'failure' continue-on-error: true uses: dawidd6/action-download-artifact@v3 with: @@ -124,10 +153,11 @@ jobs: branch: main path: baseline/ - - name: Extract baseline ABI from main branch (fallback) - if: steps.download-baseline.outcome == 'failure' + - name: Extract baseline ABI from main branch (fallback 2) + if: steps.download-merge-base.outcome == 'failure' && steps.download-main.outcome == 'failure' run: | - echo "Baseline ABI artifact not found, extracting from main branch..." + echo "⚠️ No baseline artifacts found, extracting from main branch..." + echo "This is slower but ensures we always have a baseline for comparison." git worktree add ../cuvs-main main mkdir -p ../cuvs-main/cpp/build cd ../cuvs-main/cpp/build @@ -142,7 +172,17 @@ jobs: --header_path ../cuvs-main/c/include \ --include_file cuvs/core/all.h \ --output_file baseline/c_abi.json.gz - echo "Baseline ABI extracted from main branch" + echo "✓ Baseline ABI extracted from main branch" + + - name: Report baseline source + run: | + if [ "${{ steps.download-merge-base.outcome }}" == "success" ]; then + echo "✓ Using baseline from merge-base commit: ${{ steps.merge-base.outputs.merge_base_sha }}" + elif [ "${{ steps.download-main.outcome }}" == "success" ]; then + echo "✓ Using latest main baseline (merge-base baseline not yet available)" + else + echo "✓ Using freshly extracted baseline from main branch" + fi - name: Analyze current branch for ABI breaking changes run: |