Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,34 @@
{"registry-relay-client-node", "registry-relay-client-py"}
)
NATIVE_BINDING_PACKAGES = EVIDENCE_BINDING_PACKAGES | RELAY_BINDING_PACKAGES
LINUX_NODE_BINDING_PACKAGES = frozenset(
{"registry-evidence-client-node", "registry-relay-client-node"}
)

# Inputs that can change the production Linux Node client recipe without
# changing either binding crate. This proof is deliberately selected from the
# actual changed paths rather than `complete`: push and merge-queue CI use
# `--all` for their Rust matrices, and an unrelated change must not rebuild two
# release addons merely because those matrices are complete.
LINUX_NODE_RELEASE_RECIPE_INPUTS = frozenset(
{
".github/scripts/ci_changes.py",
".github/workflows/ci.yml",
".github/workflows/release-candidate.yml",
".github/workflows/release-rehearsal.yml",
"Cargo.lock",
"Cargo.toml",
"release/requirements/maturin-1.9.6.txt",
"release/scripts/build-linux-node-client",
"release/scripts/smoke-evidence-client-package.js",
"release/scripts/smoke-relay-client-package.js",
"release/scripts/test_build_linux_node_client.py",
"release/scripts/test_zig_glibc_compiler.py",
"release/scripts/zig-glibc-compiler",
"rust-toolchain",
"rust-toolchain.toml",
}
)

# A package is exempt from the tutorial trigger only while no tutorial runs it.
# The Python binding is what `request-evidence-from-an-application` imports, so
Expand Down Expand Up @@ -433,6 +461,22 @@ def classify(
)
complete = run_all or force_all

# Compute this closure independently of the broad Rust selection above.
# In particular, `run_all=True` must not manufacture a release recipe
# trigger when no relevant path changed.
linux_node_seeds = {
package
for path in paths
if (package := workspace.package_for_path(path)) is not None
}
release_linux_node_clients = any(
path in LINUX_NODE_RELEASE_RECIPE_INPUTS or path.startswith(".cargo/")
for path in paths
) or bool(
workspace.affected_packages(linux_node_seeds)
& LINUX_NODE_BINDING_PACKAGES
)

identifiers = complete or any(
matches(path, *IDENTIFIER_CATALOG_INPUTS) for path in paths
)
Expand Down Expand Up @@ -597,6 +641,7 @@ def classify(
"docs_archives": docs_archives,
"editors": editors,
"client_bindings": client_bindings,
"release_linux_node_clients": release_linux_node_clients,
"evidence_tutorial": evidence_tutorial,
"identifiers": identifiers,
}
Expand Down
61 changes: 61 additions & 0 deletions .github/scripts/test_ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,67 @@ def test_an_sdk_or_verifier_change_also_runs_the_binding_job(self) -> None:
)
self.assertIn("registry-evidence-client-py", outputs["rust_packages"])

def test_linux_node_release_recipe_proof_follows_binding_dependency_closure(
self,
) -> None:
for path in (
"crates/registry-evidence-client-node/src/lib.rs",
"crates/registry-relay-client-node/src/lib.rs",
"crates/registry-evidence-client/src/client.rs",
"crates/registry-relay-client/src/client.rs",
"crates/registry-platform-httputil/src/lib.rs",
):
with self.subTest(path=path):
self.assertTrue(
classify(self.workspace, (path,))["release_linux_node_clients"]
)

def test_linux_node_release_recipe_inputs_select_the_proof(self) -> None:
for path in (
"Cargo.lock",
"Cargo.toml",
".cargo/config.toml",
"rust-toolchain",
"rust-toolchain.toml",
"release/requirements/maturin-1.9.6.txt",
"release/scripts/build-linux-node-client",
"release/scripts/smoke-evidence-client-package.js",
"release/scripts/smoke-relay-client-package.js",
"release/scripts/test_build_linux_node_client.py",
"release/scripts/test_zig_glibc_compiler.py",
"release/scripts/zig-glibc-compiler",
".github/scripts/ci_changes.py",
".github/workflows/ci.yml",
".github/workflows/release-candidate.yml",
".github/workflows/release-rehearsal.yml",
):
with self.subTest(path=path):
self.assertTrue(
classify(self.workspace, (path,))["release_linux_node_clients"]
)

def test_complete_matrix_alone_does_not_select_linux_node_release_recipe(
self,
) -> None:
for paths in (
(),
("release/notes/v0.22.0.md",),
("docs/site/src/content/docs/reference/glossary.mdx",),
(".github/workflows/unrelated.yml",),
):
with self.subTest(paths=paths):
outputs = classify(self.workspace, paths, run_all=True)
self.assertTrue(outputs["rust"])
self.assertFalse(outputs["release_linux_node_clients"])

def test_run_all_preserves_a_real_linux_node_recipe_trigger(self) -> None:
outputs = classify(
self.workspace,
("crates/registry-evidence-client/src/client.rs",),
run_all=True,
)
self.assertTrue(outputs["release_linux_node_clients"])

def test_current_contract_gates_replace_the_retired_notary_gate(self) -> None:
workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
self.assertIn("\n evidence-contracts:\n", workflow)
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
docs_archives: ${{ steps.filter.outputs.docs_archives }}
editors: ${{ steps.filter.outputs.editors }}
client_bindings: ${{ steps.filter.outputs.client_bindings }}
release_linux_node_clients: ${{ steps.filter.outputs.release_linux_node_clients }}
evidence_tutorial: ${{ steps.filter.outputs.evidence_tutorial }}
identifiers: ${{ steps.filter.outputs.identifiers }}
steps:
Expand Down Expand Up @@ -646,6 +647,12 @@ jobs:
- name: Test release rehearsal workflow
run: python3 -m unittest release/scripts/test_release_rehearsal.py

- name: Test Linux Node client release build helper
run: python3 -m unittest release/scripts/test_build_linux_node_client.py

- name: Test Zig glibc compiler wrapper
run: python3 -m unittest release/scripts/test_zig_glibc_compiler.py

- name: Test release workflow structure
run: python3 -m unittest release/scripts/test_release_workflow_structure.py

Expand Down Expand Up @@ -1057,6 +1064,75 @@ jobs:
)
done

release-linux-node-clients:
name: Release Linux Node clients (${{ matrix.asset }})
needs: changes
if: needs.changes.outputs.release_linux_node_clients == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 40
permissions:
contents: read
env:
RUSTUP_TOOLCHAIN: "1.95.0"
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
asset: linux-amd64-glibc
target: x86_64-unknown-linux-gnu
napi_platform: linux-x64-gnu
- runner: ubuntu-24.04-arm
asset: linux-arm64-glibc
target: aarch64-unknown-linux-gnu
napi_platform: linux-arm64-gnu
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
submodules: false

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 22.20.0
cache: npm
cache-dependency-path: |
crates/registry-evidence-client-node/package-lock.json
crates/registry-relay-client-node/package-lock.json

- name: Install pinned Linux client build tools
shell: bash
run: |
set -euo pipefail
rustup toolchain install 1.95.0 --profile minimal
python3 -m venv "${RUNNER_TEMP}/maturin"
"${RUNNER_TEMP}/maturin/bin/pip" install --quiet \
--require-hashes --only-binary=:all: \
--requirement "${GITHUB_WORKSPACE}/release/requirements/maturin-1.9.6.txt"

- name: Prove production Linux Node client recipe
shell: bash
run: |
set -euo pipefail
for client in evidence relay; do
client_dir="${GITHUB_WORKSPACE}/crates/registry-${client}-client-node"
(cd "${client_dir}" && npm ci)
release/scripts/build-linux-node-client \
--client "${client}" \
--target "${{ matrix.target }}" \
--napi-platform "${{ matrix.napi_platform }}" \
--zig-python "${RUNNER_TEMP}/maturin/bin/python"
smoke="${RUNNER_TEMP}/node-smoke-${client}"
mkdir -p "${smoke}/node_modules/@registrystack"
ln -s "${client_dir}" \
"${smoke}/node_modules/@registrystack/${client}-client"
cp "${GITHUB_WORKSPACE}/release/scripts/smoke-${client}-client-package.js" \
"${smoke}/"
(cd "${smoke}" && node "smoke-${client}-client-package.js")
done

ci-result:
name: CI result
if: always()
Expand All @@ -1075,6 +1151,7 @@ jobs:
- docs
- editor-extensions
- client-bindings
- release-linux-node-clients
runs-on: ubuntu-24.04
env:
CI_JOB_RESULTS: ${{ toJSON(needs) }}
Expand Down
49 changes: 10 additions & 39 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -475,9 +475,9 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: registry-stack-release-clients-napi-cross-glibc-2.17-${{ matrix.asset }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'crates/registry-evidence-client-node/package-lock.json', 'crates/registry-relay-client-node/package-lock.json') }}
key: registry-stack-release-clients-zig-0.12.1-glibc-2.17-${{ matrix.asset }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'release/requirements/maturin-1.9.6.txt', 'release/scripts/zig-glibc-compiler', 'release/scripts/build-linux-node-client', 'crates/registry-evidence-client-node/package-lock.json', 'crates/registry-relay-client-node/package-lock.json') }}
restore-keys: |
registry-stack-release-clients-napi-cross-glibc-2.17-${{ matrix.asset }}-
registry-stack-release-clients-zig-0.12.1-glibc-2.17-${{ matrix.asset }}-

- name: Build Python client wheels
shell: bash
Expand Down Expand Up @@ -572,44 +572,15 @@ jobs:
client_dir="${GITHUB_WORKSPACE}/crates/registry-${client}-client-node"
(cd "${client_dir}" && npm ci)
test "$(node -p "require('${client_dir}/package.json').version")" = "${CLIENT_VERSION}"
napi_args=(--platform --release --target "${{ matrix.target }}")
if [[ "${RUNNER_OS}" == Linux ]]; then
# The published linux-*-gnu package names carry no distro floor.
# aws-lc-sys treats a same-architecture target as a native build,
# so route its host C and C++ compilers through napi-rs' locked
# glibc 2.17 toolchain too. Otherwise it inherits the Ubuntu 24.04
# headers even though Rust links with the older toolchain.
export HOST_CC="${{ matrix.target }}-gcc"
export HOST_CXX="${{ matrix.target }}-g++"
napi_args+=(--use-napi-cross)
fi
(cd "${client_dir}" && ./node_modules/.bin/napi build "${napi_args[@]}")
addon="${client_dir}/${client}-client.${{ matrix.napi_platform }}.node"
if [[ "${RUNNER_OS}" == Linux ]]; then
# The version table misses unversioned imports. Candidate
# packaging rejects strong ones except the Node-API imports that
# the host intentionally resolves; weak imports remain optional.
# Rebuild with the pinned compilers is recovery, until the build
# host itself enforces the ABI floor.
unversioned_imports="$(
readelf --wide --dyn-syms "${addon}" \
| awk '$7 == "UND" && $5 != "WEAK" && $8 !~ /@/ && $8 !~ /^(napi_|node_api_)/ { print $8 }' \
| sort -u
)"
if [[ -n "${unversioned_imports}" ]]; then
printf 'native addon has strong unversioned imports:\n%s\n' \
"${unversioned_imports}" >&2
exit 1
fi
highest_glibc="$(
readelf --version-info "${addon}" \
| grep -oE 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' \
| sort -Vu \
| tail -1
)"
test -n "${highest_glibc}"
test "$(printf '%s\n' GLIBC_2.17 "${highest_glibc}" | sort -V | tail -1)" = \
GLIBC_2.17
release/scripts/build-linux-node-client \
--client "${client}" \
--target "${{ matrix.target }}" \
--napi-platform "${{ matrix.napi_platform }}" \
--zig-python "${RUNNER_TEMP}/maturin/bin/python"
else
(cd "${client_dir}" && ./node_modules/.bin/napi build \
--platform --release --target "${{ matrix.target }}")
fi
(cd "${client_dir}" && npm pack --pack-destination "${RUNNER_TEMP}")
packed="${RUNNER_TEMP}/registrystack-${client}-client-${CLIENT_VERSION}.tgz"
Expand Down
Loading