Skip to content

ci(release): build the Binding RC Linux wheel and addon with maturin and napi - #1651

Merged
DecisionNerd merged 5 commits into
mainfrom
ci/1645-binding-rc-maturin-napi
Sep 30, 2026
Merged

DecisionNerd merged 5 commits into
mainfrom
ci/1645-binding-rc-maturin-napi

Conversation

@DecisionNerd

@DecisionNerd DecisionNerd commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1645
Part of #1618

What changes

Binding RC now builds the Linux Python wheel with maturin and the Linux x64 Node addon with napi, the same way macOS and Windows already do (ADR 0048, decision item 4). The workflow has no Bazel step left.

  • Python: every lane runs PyO3/maturin-action. The Linux lane uses manylinux: "2_17", which runs the build in the manylinux2014 (glibc 2.17) container and passes --manylinux 2_17. maturin's audit then refuses the wheel if any symbol needs a newer glibc. A new step, Require the declared wheel platform tag, checks that each lane's wheel file name and the expanded dist-info/WHEEL Tag: lines exactly match the tag set declared in the matrix.
  • Node: the Linux x64 lane runs napi build --platform --release --target x86_64-unknown-linux-gnu. It uploads the napi-generated index.js and index.d.ts that its native contract ran against. The release assembly packs exactly those files. Before, it synthesized loaders with binding_rc_bazel_native.py emit-node-loaders.
  • scripts/ci/binding_rc_bazel_native.py is deleted, along with its coverage in test-binding-release-candidate.py. That test now asserts that the workflow contains no bazel at all.
  • The Binding RC section of .github/workflows/README.md is updated to match.

Defects found in the Bazel-built RC artifacts (run 36638329649, main fcb3d9e6)

  1. The Linux wheel's tag was wrong. It is named cp310-abi3-manylinux_2_17_x86_64, but its .so links GLIBC_2.39 (built on the Ubuntu 24.04 runner). The Bazel assembler wrote the tag without auditing the binary. auditwheel show reports: consistent with the following platform tag: "manylinux_2_39_x86_64".
  2. The Linux x64 Node addon reports version() == "0.0.0". This is why the last RC assembly failed in the offline rehearsal: clean Node/native consumer loaded version '0.0.0', expected 0.5.2. A napi build takes the version from Cargo.

Evidence (local, pre-merge)

Wheel. I ran maturin-action v1.51.0's container script (maturin 1.15.0, Rust 1.96.0) in the quay.io/pypa/manylinux2014_x86_64 rootfs, image created 2026-09-26, as maturin build --manylinux 2_17 --release --manifest-path crates/graphforge-bindings-py/Cargo.toml. This is the argument order the action produces.

Bazel RC wheel (run 36638329649) maturin, manylinux2014 (this PR)
file graphforge-0.5.2-cp310-abi3-manylinux_2_17_x86_64.whl graphforge-0.5.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
WHEEL Tag cp310-abi3-manylinux_2_17_x86_64 cp310-abi3-manylinux_2_17_x86_64 and cp310-abi3-manylinux2014_x86_64
Generator assemble_bazel_binding_packages.py maturin (1.15.0) (the same as the macOS and Windows RC wheels)
highest GLIBC symbol GLIBC_2.39 GLIBC_2.16
auditwheel show constrains the tag to manylinux_2_39_x86_64 constrains the tag to manylinux_2_17_x86_64
METADATA identical (apart from a trailing newline) identical
extra members none dist-info/sboms/graphforge-bindings-py.cyclonedx.json (the macOS wheel has it too)
graphforge.__version__ 0.5.2 0.5.2

The platform tag stays manylinux_2_17_x86_64. maturin also writes the equivalent PEP 600 legacy alias manylinux2014_x86_64, so the file name changes. Nothing in the repo matches the Linux wheel file name exactly. The RC Python contract (smoke.py, gil_release.py, multi_ontology.py, non_cypher_release.py --classification-only) passes on the local wheel.

The guard rejects a non-compliant wheel. maturin build --manylinux 2_17 --release on this Ubuntu 26.04 host (glibc 2.43) fails: Your library is not manylinux_2_17 (aka manylinux2014) compliant because of the presence of too-recent versioned symbols.

The tag-check step, run from the workflow YAML: it passes the local manylinux wheel and the maturin-built macOS (cp310-abi3-macosx_11_0_arm64) and Windows (cp310-abi3-win_amd64) wheels from run 36638329649. It rejects the Bazel Linux wheel.

Addon. I ran napi build --platform --release --target x86_64-unknown-linux-gnu on the host.

  • Output: graphforge.linux-x64-gnu.node (x86-64 ELF). It needs the same libraries as the Bazel addon (libgcc_s, libm, libc, ld-linux-x86-64).
  • version() returns 0.5.2. The Bazel addon from run 36638329649 returns 0.0.0.
  • The RC Node contract passes: ESM import, test:smoke (8/8), and non-cypher-release-parity + async-errors + export-surface (11/11). The addon, index.js and index.d.ts hashes are unchanged after the tests.
  • Simulated assembly: I copied the lane's loaders into a clean package copy, then ran napi create-npm-dirs, napi artifacts and validate-napi-artifacts.py, and packed with npm pack. The main tarball contains package/index.js and package/index.d.ts. A clean npm install of the main and linux-x64-gnu tarballs loads version() == "0.5.2" through the named ESM import.
  • The platform package metadata comes from napi create-npm-dirs, the same as before: @curatelabs/graphforge-linux-x64-gnu, os: [linux], cpu: [x64], libc: [glibc], main: graphforge.linux-x64-gnu.node.
  • The glibc floor is unchanged in kind. Both the Bazel addon and the napi addon are host builds on the blacksmith-4vcpu-ubuntu-2404 runner, so both need glibc 2.39. My local napi build needs 2.43 only because this host is newer. See the follow-up note below.

Gates: make workflow-lint (actionlint 1.7.12), python3 scripts/ci/workflow_policy.py, python3 scripts/ci/test-binding-release-candidate.py, pytest scripts/ci/test-release-workflows.py (15 passed), test-publish-track.py, test-release-certification.py, test-release-candidate.py, test-release-rehearsal.py, tests/unit/test_publish_dry_run.py (9 passed), and make pre-push-fast all pass. I mutation-tested the new policy assertions. Each of these mutations made test-binding-release-candidate.py fail: removing the compatibility input, dropping the tag-step, changing the declared tag, removing publishes_loaders, gating the napi build off Linux, disabling the ownership reclaim, and adding a Bazel step.

Dispatch qualification

Binding RC run 36655614470 runs this PR workflow at 307631419d7a8aa35a011000bd6e67f502c4eff6. Its unchanged current-main guard selects native source 471c16ff01daa1b6d76f3e654d59796d8fc096c8. All eight target lanes, the aggregate, release assembly and offline rehearsal passed. Workflow and native-source identities are separate. The PR changes no native crate, Cargo manifest/lock, package manifest or dependency-lock inputs relative to that source. Publication certification of a later merged source requires its own ordinary SHA-bound evidence.

Independent inspection of the actual downloaded Linux artifacts verifies:

Property Last Bazel RC (36638329649) This dispatch
Wheel Python/ABI cp310-abi3 cp310-abi3
Wheel platform metadata manylinux_2_17_x86_64 same, plus equivalent manylinux2014_x86_64 alias
Wheel package metadata graphforge 0.5.2, Python >=3.10, pyarrow/polars requirements identical
Highest wheel GLIBC symbol 2.39 despite declared 2.17 2.16
Node target Linux x64 GNU / AMD64 ELF same
Node highest GLIBC symbol 2.39 2.39
Native Node version() 0.0.0 0.5.2

The wheel and addon SHA-256 digests match the passed aggregate report. The exact-run tested loader digests are retained with the comparison results. The assembled Linux x64 package retains linux/x64/glibc metadata and the declared native filename. Its native binary and the main package loaders match the tested artifacts exactly by SHA-256. The offline rehearsal passed clean Python, Node, CLI/skills and Rust consumers, with zero registry writes. Full results and digests are posted on #1645.

Storage policy

The policy permits only the binding-rc-node-loaders-${{ github.run_id }} transfer with exactly index.js and index.d.ts, its declared upload and ${{ runner.temp }}/node-loaders destination, and current-run/default-repository download semantics. Eighteen negative fixtures reject native binaries, directory/glob payloads, changed destinations and cross-run/repository overrides. The Linux sticky-disk expectation is one. The accidentally tracked admission lock is removed.

Remaining retirement

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: CurateLabs/graphforge/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 0e64ae66-24f3-4ec1-b5de-70eeff20f2a2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added core Core source code changes documentation Improvements or additions to documentation ci-cd CI/CD configuration changes tooling Developer tooling and automation labels Sep 30, 2026
@DecisionNerd

Copy link
Copy Markdown
Contributor Author

Independent integration review at cbe3459 found two merge blockers:

  1. .github/workflows/binding-release-candidate.yml wheel-tag step rejects normal maturin output with equivalent compressed manylinux aliases. Running the exact step on graphforge-0.5.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl exits 1 with does not carry tag cp310-abi3-manylinux_2_17_x86_64; the subsequent single-element WHEEL-tag comparison also rejects the alias. Preserve the compatibility audit and require the declared tag while permitting only the equivalent manylinux2014 alias. Upstream implementation: https://github.com/PyO3/maturin/blob/main/src/target/wheel_tag.rs.
  2. Repository Policy fails in job 109684171848: the new upload of crates/graphforge-bindings-node/index.js and index.d.ts has no narrow artifact contract. Add exactly those loader paths with the run-scoped artifact name to test-ci-storage-policy.py, preserving its build-output restrictions.

Both were independently reproduced from the immutable PR head. Linux wheel/addon execution evidence remains required by #1645; the review does not claim a green Binding RC dispatch.

@DecisionNerd

Copy link
Copy Markdown
Contributor Author

Follow-up review at d8b8c4c78eb4d70473a86f0676decb8bf708bbb8: I independently opened the locally built wheel and last Bazel wheel. The new wheel declares both cp310-abi3-manylinux_2_17_x86_64 and its equivalent cp310-abi3-manylinux2014_x86_64 alias, so the compressed tag expansion now matches the actual artifact.

Two findings remain before integration:

  • The run-scoped Node loader upload is still missing its narrow policy allowance; the new head leaves test-ci-storage-policy.py unchanged. Preserve the ban on generic build-directory artifacts while allowing only the generated index.js and index.d.ts handoff.
  • The last commit includes an unrelated generated admission lock under crates/graphforge-bindings-node/.graphforge-admission-*.lock. Remove it from the diff.

The first #1645 criterion requests a dispatch on the PR head, while validate_source deliberately accepts only current main. Keep that source guard. A dispatch of the PR workflow using current-main commit_sha can exercise the changed native-builder matrix before merge; report the workflow head and measured source separately, then qualify the merged head using the standard main-only dispatch. Local wheel/addon comparisons are useful interim evidence but do not satisfy the dispatch criterion by themselves. The PR body still has EVIDENCE_PLACEHOLDER and the superseded --compatibility command.

DecisionNerd and others added 4 commits September 30, 2026 01:09
…and napi

Binding RC built the Linux Python wheel and the Linux x64 Node addon with
Bazel through scripts/ci/binding_rc_bazel_native.py. Both now use the same
maturin and napi path as macOS and Windows (ADR 0048, decision item 4).

- Python: every lane runs maturin-action. Linux builds in the manylinux2014
  container with `--compatibility manylinux_2_17`, so maturin's audit refuses
  a wheel that needs a newer glibc rather than relabelling it. The Bazel-built
  wheel was tagged manylinux_2_17 but links GLIBC_2.39 (auditwheel: consistent
  only with manylinux_2_39_x86_64). Each lane now asserts its declared wheel
  tag in the file name and in dist-info/WHEEL.
- Node: the Linux x64 lane runs `napi build`, which also fixes the Bazel
  addon reporting version() == "0.0.0" that failed every recent RC assembly
  in the offline rehearsal. The lane uploads the napi-generated
  index.js/index.d.ts its native contract executed, and the release assembly
  packs exactly those instead of synthesizing loaders.
- Retire binding_rc_bazel_native.py and its test coverage; the workflow has no
  Bazel step and the policy test asserts that.

Closes #1645
Part of #1618

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…linux 2_17 once

A local build of the exact maturin-action command in the manylinux2014 image
names the wheel cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64 and
writes both Tag: lines. Declare that tag set and check the expanded set, and
let the action's manylinux input supply the single --manylinux 2_17 flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Match the other Binding RC same-run downloads (pattern + merge-multiple).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e Linux RC sticky disk

The Linux x64 Node lane uploads the napi loaders it tested and the assembly
downloads them by exact-run pattern; the retired Python sticky mount leaves
one binding-rc-linux-rust disk.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@DecisionNerd
DecisionNerd force-pushed the ci/1645-binding-rc-maturin-napi branch from c4128bd to 26b131e Compare September 30, 2026 01:10
@DecisionNerd

DecisionNerd commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Reviewed author head 26b131e: the loader counters, sticky census and existing allowance now pass policy, but the allowance still accepts addon bytes under the loader name, a generic dist destination and an unrelated repository override. I reproduced those against the actual workflow. A bounded additive correction is prepared in my isolated checkout, commit 3076314: it binds the loader producer to index.js/index.d.ts, binds the consumer to the current run and exact destination, and removes the accidental admission lock. All relevant static checks and negative fixtures pass. I will preserve the running independent CI lanes and push the correction after they finish; no external author worktree was changed.

@github-actions github-actions Bot added the release:none No release note or version impact label Sep 30, 2026
@DecisionNerd

Copy link
Copy Markdown
Contributor Author

The additive artifact-policy correction is now pushed at 307631419d7a8aa35a011000bd6e67f502c4eff6; it restricts the Node loader transfer to exactly index.js / index.d.ts, its declared destination and the current workflow run, with 18 negative mutation fixtures. It also removes the accidentally tracked admission lock. Existing independent lanes at 26b131ee all completed successfully before the push; final exact-head CI is now running.

Binding RC dispatch 36655614470 executes this PR workflow revision, while preserving the current-main certification guard: its requested native-source SHA is 471c16ff01daa1b6d76f3e654d59796d8fc096c8. This is workflow qualification against current main; it does not certify unmerged native source. We will record both identities, let independent target lanes finish, and inspect one complete failure census if necessary. A merged-source dispatch remains necessary for any later publication claim.

The current body inaccurately says the PR workflow cannot be dispatched at all: it can run with the main source SHA as above. Remaining assembler/platform-parity retirement is already included in #1646, so canonical #1618 stays open until that removal is merged; no remaining Bazel source is being declared retired here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd CI/CD configuration changes core Core source code changes documentation Improvements or additions to documentation release:none No release note or version impact tooling Developer tooling and automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(release): build the Binding RC Linux wheel and addon with maturin and napi (#1618)

1 participant