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
41 changes: 32 additions & 9 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"registry-manifest-core",
),
"relay": ("registry-relay",),
"relay-client": (
"registry-relay-http-contract",
"registry-relay-client",
"registry-relay-client-node",
"registry-relay-client-py",
),
"relay-v2": ("registry-relay-v2", "registry-relayctl"),
"evidence": (
"registry-evidence",
Expand All @@ -56,6 +62,7 @@
PLATFORM_PACKAGES = frozenset(SHARDS["platform"])
MANIFEST_PACKAGES = frozenset(SHARDS["manifest"])
RELAY_V2_PACKAGES = frozenset(SHARDS["relay-v2"])
RELAY_CLIENT_PACKAGES = frozenset(SHARDS["relay-client"])

# Every input the Evidence tutorial gate replays or is built from. The tutorial
# pages and helper scripts here must stay in step with the gate's own registry
Expand Down Expand Up @@ -127,6 +134,10 @@
EVIDENCE_BINDING_PACKAGES = frozenset(
{"registry-evidence-client-node", "registry-evidence-client-py"}
)
RELAY_BINDING_PACKAGES = frozenset(
{"registry-relay-client-node", "registry-relay-client-py"}
)
NATIVE_BINDING_PACKAGES = EVIDENCE_BINDING_PACKAGES | RELAY_BINDING_PACKAGES

# 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 @@ -263,6 +274,7 @@ def __init__(self, metadata: dict[str, Any]) -> None:
).as_posix()

reverse_dependencies: dict[str, set[str]] = defaultdict(set)
dev_reverse_dependencies: dict[str, set[str]] = defaultdict(set)
for package_name, package in packages.items():
for dependency in package["dependencies"]:
dependency_name = dependency["name"]
Expand All @@ -273,8 +285,12 @@ def __init__(self, metadata: dict[str, Any]) -> None:
and Path(dependency_path).resolve()
== Path(packages[dependency_name]["manifest_path"]).resolve().parent
):
reverse_dependencies[dependency_name].add(package_name)
if dependency.get("kind") == "dev":
dev_reverse_dependencies[dependency_name].add(package_name)
else:
reverse_dependencies[dependency_name].add(package_name)
self.reverse_dependencies = reverse_dependencies
self.dev_reverse_dependencies = dev_reverse_dependencies

def package_for_path(self, path: str) -> str | None:
matches = [
Expand All @@ -288,13 +304,19 @@ def package_for_path(self, path: str) -> str | None:

def affected_packages(self, seeds: Iterable[str]) -> set[str]:
affected = set(seeds)
queue = deque(affected)
propagating = set(seeds)
queue = deque(propagating)
while queue:
dependency = queue.popleft()
for dependent in self.reverse_dependencies.get(dependency, ()):
if dependent not in affected:
if dependent not in propagating:
affected.add(dependent)
propagating.add(dependent)
queue.append(dependent)
# A dev-dependency must schedule the immediate consumer's tests,
# but it is not linked into that consumer's library. Do not let
# this test-only edge fan out through the consumer's dependents.
affected.update(self.dev_reverse_dependencies.get(dependency, ()))
return affected


Expand Down Expand Up @@ -434,7 +456,7 @@ def classify(
# description.
"products/relay-v2/CONCEPT.md",
"products/relay-v2/STANDARDS-ALIGNMENT.md",
# No page is generated from these four, but scripts/
# No page is generated from these files, but scripts/
# ops-posture-spec.test.mjs reads them to prove the published
# operational claims still match the runtime. A probe route, a
# runtime bound, or a healthcheck default can change here and
Expand All @@ -444,6 +466,7 @@ def classify(
"crates/registry-relay-v2/src/main.rs",
"crates/registry-relay-v2/src/contract.rs",
"crates/registry-relay-v2/src/startup.rs",
"crates/registry-relay-http-contract/src/lib.rs",
}
for path in paths
)
Expand Down Expand Up @@ -478,11 +501,10 @@ def classify(
or any(path.startswith("editors/") for path in paths)
or "registry-language-server" in affected
)
# Reverse dependents, not changed paths: both bindings are Cargo path
# dependents of the SDK and the verifier, so a change to either can move
# the native surface or the error envelope the packages wrap without
# touching a file inside a binding crate.
client_bindings = complete or bool(affected & EVIDENCE_BINDING_PACKAGES)
# Reverse dependents, not changed paths: bindings are Cargo path dependents
# of each SDK, so an SDK or shared HTTP-contract change can move a native
# surface without touching a binding crate.
client_bindings = complete or bool(affected & NATIVE_BINDING_PACKAGES)

evidence_tutorial = (
complete
Expand Down Expand Up @@ -510,6 +532,7 @@ def classify(
"platform_hygiene": platform_hygiene,
"relay_contracts": "registry-relay" in affected,
"relay_v2_contracts": bool(affected & RELAY_V2_PACKAGES),
"relay_client_contracts": bool(affected & RELAY_CLIENT_PACKAGES),
"evidence_contracts": bool(affected & EVIDENCE_PACKAGES),
"project_authoring": "registryctl" in affected,
"release_tool": release_tool,
Expand Down
85 changes: 64 additions & 21 deletions .github/scripts/test_ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,10 @@ def normal_dependency_metadata(metadata: dict[str, Any]) -> dict[str, Any]:

Cargo reports normal, build and dev dependencies in one list per package
and tells them apart with a `kind` of null, "build" or "dev". The
classifier keeps all three on purpose, because a dev-dependency edge is
still a reason to run the dependent's tests. That makes its closure the
wrong witness for a claim about what a shipped binary contains: it cannot
see the difference between a crate an editor session compiles in and a
crate only a test harness pulls in. A test that has to prove the stronger
claim classifies against this reduced workspace as well, so a link moved
out of `[dependencies]` fails it however many test-only edges survive.
classifier schedules a direct dev-dependent's tests without propagating
through it, while this reduced workspace proves claims about code a
shipped binary actually links. A link moved out of `[dependencies]` must
therefore fail the stronger routing claim even if test-only edges remain.
"""

packages = [
Expand Down Expand Up @@ -243,7 +240,7 @@ def test_identifier_exporters_and_indirect_inputs_select_the_catalog_gate(
"crates/registry-relay-v2/examples/problem-catalog.rs",
"crates/registry-relay-v2/src/artifacts.rs",
"crates/registry-relay-v2/src/audit.rs",
"crates/registry-relay-v2/src/problem.rs",
"crates/registry-relay-http-contract/src/lib.rs",
):
with self.subTest(path=path):
self.assertTrue(classify(self.workspace, (path,))["identifiers"])
Expand Down Expand Up @@ -393,6 +390,24 @@ def test_reverse_dependencies_are_included(self) -> None:
self.assertIn("registry-platform-crypto", outputs["rust_packages"])
self.assertIn("registry-relay", outputs["rust_packages"])

def test_platform_changes_select_relay_client_reverse_dependents(self) -> None:
# The Relay SDK deliberately reuses the shared bounded outbound and
# OAuth primitives. A platform change can therefore alter its wire
# behavior without touching the SDK source, and must retain the native
# bindings in its affected closure.
outputs = classify(
self.workspace,
("crates/registry-platform-httputil/src/lib.rs",),
)
for package in (
"registry-relay-client",
"registry-relay-client-node",
"registry-relay-client-py",
):
with self.subTest(package=package):
self.assertIn(package, outputs["rust_packages"])
self.assertTrue(outputs["client_bindings"])

def test_ci_workflow_change_runs_the_complete_matrix(self) -> None:
outputs = classify(self.workspace, (".github/workflows/ci.yml",))
self.assertCountEqual(outputs["rust_packages"], self.workspace.package_names)
Expand Down Expand Up @@ -467,13 +482,9 @@ def test_an_authoring_form_change_runs_the_editor_tooling_that_reads_it(self) ->
self.assertIn("registry-language-server", outputs["rust_packages"])
self.assertIn("registryctl", outputs["rust_packages"])

# The language server also dev-depends on the authoring form, for the
# testing feature its own suite drives, and the classifier's closure
# reads every dependency table alike. The assertions above therefore
# hold on that test-only edge by itself, which is a weaker fact than
# the one this test is named for: a test-only edge puts nothing inside
# an adopter's editor. Repeating the closure over normal edges alone
# ties the shards to the link the editor actually compiles against.
# The language server also dev-depends on the authoring form for its
# own test suite. Repeating the closure over normal edges alone ties
# the editor routing claim to the link the editor actually compiles.
strict = classify(
Workspace(normal_dependency_metadata(self.metadata)),
AUTHORING_FORM_CHANGE,
Expand Down Expand Up @@ -527,11 +538,8 @@ def test_a_test_only_editor_edge_does_not_satisfy_the_authoring_routing(
# The check above is only worth its name if it can tell the two edges
# apart, so hold it against the workspace where it must not hold: the
# language server keeps the test-only dependency and loses the one it
# compiles against. Both halves matter here. The kind-blind closure
# still reaches every editor shard, which is the reason the routing
# claim cannot rest on it, and the normal-edge closure stops at the
# authoring form's own shard, which is the power the routing claim
# borrows from it.
# compiles against. A dev edge still selects that direct test suite,
# but cannot make registryctl a downstream affected package.
mutated = dev_only_dependency_metadata(
self.metadata,
consumer="registry-language-server",
Expand All @@ -540,7 +548,7 @@ def test_a_test_only_editor_edge_does_not_satisfy_the_authoring_routing(

blind = classify(Workspace(mutated), AUTHORING_FORM_CHANGE)
self.assertIn("registry-language-server", blind["rust_packages"])
self.assertIn("registryctl", blind["rust_packages"])
self.assertNotIn("registryctl", blind["rust_packages"])

strict = classify(
Workspace(normal_dependency_metadata(mutated)),
Expand Down Expand Up @@ -581,6 +589,30 @@ def test_binding_only_change_runs_contracts_but_not_the_tutorial_job(self) -> No
{"evidence"},
)

def test_relay_client_change_runs_its_contract_and_native_binding_gates(self) -> None:
outputs = classify(
self.workspace,
("crates/registry-relay-client/src/lib.rs",),
)
self.assertTrue(outputs["relay_client_contracts"])
self.assertTrue(outputs["client_bindings"])
# Relay V2 owns the real-router acceptance test and therefore
# dev-depends on the SDK. Its test suite must still run, but the
# dev-only edge cannot cascade into Relay V2's normal dependents.
self.assertIn("registry-relay-v2", outputs["rust_packages"])
self.assertNotIn("registry-relayctl", outputs["rust_packages"])
self.assertFalse(outputs["evidence_contracts"])
self.assertEqual(
{entry["name"] for entry in outputs["rust_matrix"]["include"]},
{"relay-client", "relay-v2"},
)
relay_client_matrix = next(
entry
for entry in outputs["rust_matrix"]["include"]
if entry["name"] == "relay-client"
)
self.assertFalse(relay_client_matrix["all_features"])

def test_oid4vci_change_runs_rust_contracts_and_its_registered_tutorial(self) -> None:
outputs = classify(
self.workspace,
Expand Down Expand Up @@ -652,6 +684,10 @@ def test_current_contract_gates_replace_the_retired_notary_gate(self) -> None:
)
self.assertIn("\n relay-contracts:\n", workflow)
self.assertIn("name: Relay OpenAPI contract", workflow)
self.assertIn("\n relay-client-contracts:\n", workflow)
self.assertIn(
"products/relay-v2/scripts/check-client-contract.sh", workflow
)
self.assertNotIn("\n notary-contracts:\n", workflow)
self.assertNotIn("notary_contracts", workflow)

Expand All @@ -660,6 +696,7 @@ def test_current_contract_gates_replace_the_retired_notary_gate(self) -> None:
)[0]
self.assertIn("\n - evidence-contracts\n", rust_result)
self.assertIn("\n - relay-contracts\n", rust_result)
self.assertIn("\n - relay-client-contracts\n", rust_result)
self.assertNotIn("\n - notary-contracts\n", rust_result)

def test_archive_content_is_immutable_during_routine_docs_changes(self) -> None:
Expand Down Expand Up @@ -890,6 +927,12 @@ def test_relay_docs_routing_matrix(self) -> None:
"crates/registry-relay-v2/src/server.rs",
{"docs": True, "relay_v2_contracts": True},
),
(
# The same test reads the shared probe-route constants from
# the standalone HTTP contract.
"crates/registry-relay-http-contract/src/lib.rs",
{"docs": True, "relay_client_contracts": True},
),
(
# A Relay V2 source no docs test reads stays out of the docs job.
"crates/registry-relay-v2/src/api.rs",
Expand Down
70 changes: 56 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
platform_hygiene: ${{ steps.filter.outputs.platform_hygiene }}
relay_contracts: ${{ steps.filter.outputs.relay_contracts }}
relay_v2_contracts: ${{ steps.filter.outputs.relay_v2_contracts }}
relay_client_contracts: ${{ steps.filter.outputs.relay_client_contracts }}
evidence_contracts: ${{ steps.filter.outputs.evidence_contracts }}
project_authoring: ${{ steps.filter.outputs.project_authoring }}
release_tool: ${{ steps.filter.outputs.release_tool }}
Expand Down Expand Up @@ -570,6 +571,32 @@ jobs:
- name: Relay V2 coequal HTTP journeys
run: products/relay-v2/scripts/test-http.sh

relay-client-contracts:
name: Relay client contract and source neutrality
needs: changes
if: needs.changes.outputs.relay_client_contracts == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
submodules: false

- name: Cache Cargo registry
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: workspace-registry
cache-targets: false
save-if: ${{ github.ref == 'refs/heads/main' }}

- name: Relay client contract consistency
run: products/relay-v2/scripts/check-client-contract.sh

- name: Relay client source neutrality
run: products/relay-v2/scripts/check-source-neutrality.sh

identifiers:
name: Public identifier catalog
needs: changes
Expand Down Expand Up @@ -604,6 +631,7 @@ jobs:
- evidence-contracts
- relay-contracts
- relay-v2-contracts
- relay-client-contracts
- identifiers
runs-on: ubuntu-24.04
env:
Expand Down Expand Up @@ -1188,7 +1216,7 @@ jobs:
cmp LICENSE editors/zed/LICENSE

client-bindings:
name: Evidence client bindings
name: Native client bindings
needs: changes
if: needs.changes.outputs.client_bindings == 'true'
runs-on: ubuntu-24.04
Expand All @@ -1204,23 +1232,37 @@ jobs:
with:
node-version: 22.12.0
cache: npm
cache-dependency-path: crates/registry-evidence-client-node/package-lock.json
cache-dependency-path: |
crates/registry-evidence-client-node/package-lock.json
crates/registry-relay-client-node/package-lock.json

- name: Build and test the Node binding
working-directory: crates/registry-evidence-client-node
- name: Build and test Node bindings
shell: bash
run: |
npm ci
npm run build:debug
npm test
npm run check:types
cmp ../../LICENSE LICENSE
set -euo pipefail
for client in registry-evidence-client-node registry-relay-client-node; do
(
cd "crates/${client}"
npm ci
npm run build:debug
npm test
npm run check:types
cmp ../../LICENSE LICENSE
)
done

- name: Build and test the Python binding
working-directory: crates/registry-evidence-client-py
- name: Build and test Python bindings
shell: bash
run: |
cargo build --locked -p registry-evidence-client-py --lib --features registry-evidence-client-py/extension-module
python3 -m unittest discover -s tests/python -v
cmp ../../LICENSE LICENSE
set -euo pipefail
for client in registry-evidence-client-py registry-relay-client-py; do
cargo build --locked -p "${client}" --lib --features "${client}/extension-module"
(
cd "crates/${client}"
python3 -m unittest discover -s tests/python -v
cmp ../../LICENSE LICENSE
)
done

ci-result:
name: CI result
Expand Down
Loading
Loading