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
58 changes: 58 additions & 0 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,55 @@
AUTHORING_REFERENCE_MANIFEST = (
REPO_ROOT / "docs/site/scripts/authoring-reference-sources.json"
)
IDENTIFIER_CATALOG_CONTRACT = (
REPO_ROOT / "products/identifiers/contracts/catalog-source.json"
)


def identifier_catalog_inputs(
contract_path: Path = IDENTIFIER_CATALOG_CONTRACT,
) -> tuple[str, ...]:
"""Derive every source path that can change the public identifier catalog."""

contract = json.loads(contract_path.read_text(encoding="utf-8"))
schema_groups = contract.get("schemaSources")
records = contract.get("records")
if not isinstance(schema_groups, list) or not isinstance(records, list):
raise ValueError(
f"identifier catalog has an invalid source contract: {contract_path}"
)

inputs = [
"products/identifiers/**",
"crates/registry-relay-v2/examples/audit-event-schema.rs",
"crates/registry-relay-v2/examples/problem-catalog.rs",
"crates/registry-relay-v2/src/audit.rs",
"crates/registry-relay-v2/src/problem.rs",
]
Comment thread
jeremi marked this conversation as resolved.
for index, group in enumerate(schema_groups):
pattern = group.get("glob") if isinstance(group, dict) else None
if not isinstance(pattern, str) or not pattern:
raise ValueError(f"identifier schemaSources[{index}] has no glob")
inputs.append(pattern)
source = group.get("sourcePath")
if source is not None:
if not isinstance(source, str) or not source:
raise ValueError(
f"identifier schemaSources[{index}] has an invalid sourcePath"
)
inputs.append(source)
for index, record in enumerate(records):
source = record.get("sourcePath") if isinstance(record, dict) else None
if not isinstance(source, str) or not source:
raise ValueError(f"identifier records[{index}] has no sourcePath")
inputs.append(source)

if any(source.startswith(("/", "../")) for source in inputs):
raise ValueError("identifier catalog inputs must be repository-relative")
return tuple(dict.fromkeys(inputs))


IDENTIFIER_CATALOG_INPUTS = identifier_catalog_inputs()


def authoring_reference_contract_sources(
Expand Down Expand Up @@ -405,6 +454,10 @@ def classify(
seeds.update(PLATFORM_PACKAGES)
elif path.startswith("products/relay-v2/"):
seeds.update(RELAY_V2_PACKAGES)
elif path.startswith("products/identifiers/"):
# The catalog gate compiles its focused Relay V2 exporter.
# Catalog-only tooling does not require the full Rust matrix.
pass
elif path in {
"docs/site/src/data/generated/relay-support.json",
"docs/site/src/data/relay-support.yaml",
Expand All @@ -421,6 +474,10 @@ def classify(
)
complete = run_all or force_all

identifiers = complete or any(
matches(path, *IDENTIFIER_CATALOG_INPUTS) for path in paths
)

platform = complete or any(
matches(
path,
Expand Down Expand Up @@ -644,6 +701,7 @@ def classify(
"client_bindings": client_bindings,
"registryctl_tutorial": registryctl_tutorial,
"evidence_tutorial": evidence_tutorial,
"identifiers": identifiers,
}


Expand Down
35 changes: 35 additions & 0 deletions .github/scripts/test_ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AUTHORING_REFERENCE_INPUTS,
EVIDENCE_AUTHORING_GUIDE_IMPLEMENTATION_INPUTS,
EVIDENCE_TUTORIAL_INPUTS,
IDENTIFIER_CATALOG_INPUTS,
RELEASE_SECURITY_WORKFLOWS,
SHARDS,
Workspace,
Expand Down Expand Up @@ -232,6 +233,40 @@ def test_relay_v2_paths_select_the_editor_and_reverse_dependents(self) -> None:
]:
self.assertIn(package, outputs["rust_packages"])

def test_every_identifier_source_selects_the_catalog_gate(self) -> None:
for pattern in IDENTIFIER_CATALOG_INPUTS:
sample = pattern.replace("**", "sample").replace("*", "sample")
with self.subTest(pattern=pattern):
self.assertTrue(classify(self.workspace, (sample,))["identifiers"])

def test_identifier_exporters_and_indirect_inputs_select_the_catalog_gate(
self,
) -> None:
for path in (
"crates/registry-relay-v2/examples/audit-event-schema.rs",
"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",
):
with self.subTest(path=path):
self.assertTrue(classify(self.workspace, (path,))["identifiers"])

def test_ci_always_checks_repository_identifier_reference_closure(self) -> None:
workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
self.assertIn(
"products/identifiers/scripts/generate.py --check-references",
workflow,
)

def test_identifier_tooling_does_not_force_the_rust_matrix(self) -> None:
outputs = classify(
self.workspace,
("products/identifiers/scripts/generate.py",),
)
self.assertTrue(outputs["identifiers"])
self.assertFalse(outputs["rust"])

def test_relay_v2_product_material_selects_runtime_and_tooling(self) -> None:
outputs = classify(
self.workspace,
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
client_bindings: ${{ steps.filter.outputs.client_bindings }}
registryctl_tutorial: ${{ steps.filter.outputs.registryctl_tutorial }}
evidence_tutorial: ${{ steps.filter.outputs.evidence_tutorial }}
identifiers: ${{ steps.filter.outputs.identifiers }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
Expand Down Expand Up @@ -99,6 +100,9 @@ jobs:
fi
"${classifier[@]}"

- name: Check repository identifier reference closure
run: products/identifiers/scripts/generate.py --check-references

- name: Test CI classifier
run: python3 .github/scripts/test_ci_changes.py

Expand Down Expand Up @@ -567,6 +571,29 @@ jobs:
- name: Relay V2 coequal HTTP journeys
run: products/relay-v2/scripts/test-http.sh

identifiers:
name: Public identifier catalog
needs: changes
if: needs.changes.outputs.identifiers == '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: Check public identifier catalog
run: products/identifiers/scripts/check.sh

rust-result:
name: Rust workspace
if: always()
Expand All @@ -578,6 +605,7 @@ jobs:
- evidence-contracts
- relay-contracts
- relay-v2-contracts
- identifiers
runs-on: ubuntu-24.04
env:
RUST_JOB_RESULTS: ${{ toJSON(needs) }}
Expand Down
4 changes: 0 additions & 4 deletions crates/registry-manifest-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ const BUILTIN_VOCABULARIES: &[(&str, &str)] = &[
"registry_manifest",
"https://id.registrystack.org/ns/registry-manifest/v1#",
),
(
"registry_relay",
"https://id.registrystack.org/ns/registry-relay/v1#",
),
("sh", "http://www.w3.org/ns/shacl#"),
("skos", "http://www.w3.org/2004/02/skos/core#"),
("xsd", "http://www.w3.org/2001/XMLSchema#"),
Expand Down
6 changes: 3 additions & 3 deletions crates/registry-manifest-core/tests/metadata_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,10 +984,10 @@ fn vocabularies_protect_builtins_and_validate_custom_namespaces() {
);
manifest.vocabularies.insert(
"registry_relay".to_string(),
"https://id.registrystack.org/ns/registry-relay/v1#".to_string(),
"https://example.org/relay/ns#".to_string(),
);
validate_manifest(&manifest)
.expect("safe custom vocabulary and identical protected values pass");
.expect("safe custom vocabularies and identical protected values pass");
}

#[test]
Expand Down Expand Up @@ -2044,7 +2044,7 @@ datasets:
lookup_keys: [national_id]
access:
kind: evidence-server
conforms_to: registry_relay:evidence-server-v1
conforms_to: https://example.test/evidence-server/v1
endpoint_url: https://evidence.example.test
discovery_url: https://evidence.example.test/.well-known/evidence-service
ruleset: smallholder-v1
Expand Down
35 changes: 35 additions & 0 deletions crates/registry-relay-v2/examples/audit-event-schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0

use std::{env, fs, path::PathBuf, process::ExitCode};

use registry_relay_v2::artifacts::audit_event_schema;

fn main() -> ExitCode {
let mut arguments = env::args_os().skip(1);
let Some(flag) = arguments.next() else {
eprintln!("usage: audit-event-schema --output <file>");
return ExitCode::from(2);
};
let Some(output) = arguments.next() else {
eprintln!("usage: audit-event-schema --output <file>");
return ExitCode::from(2);
};
if flag != "--output" || arguments.next().is_some() {
eprintln!("usage: audit-event-schema --output <file>");
return ExitCode::from(2);
}

let mut bytes = match serde_json::to_vec_pretty(&audit_event_schema()) {
Ok(bytes) => bytes,
Err(_) => {
eprintln!("audit event schema could not be serialized");
return ExitCode::FAILURE;
}
};
bytes.push(b'\n');
if fs::write(PathBuf::from(output), bytes).is_err() {
eprintln!("audit event schema could not be written");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
66 changes: 66 additions & 0 deletions crates/registry-relay-v2/examples/problem-catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0

use std::{env, fs, path::PathBuf, process::ExitCode};

use registry_relay_v2::problem::ProblemCode;
use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProblemCatalog<'a> {
entries: Vec<ProblemEntry<'a>>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProblemEntry<'a> {
uri: String,
code: &'a str,
title: &'a str,
description: &'a str,
http_statuses: [u16; 1],
}

fn main() -> ExitCode {
let mut arguments = env::args_os().skip(1);
let Some(flag) = arguments.next() else {
eprintln!("usage: problem-catalog --output <file>");
return ExitCode::from(2);
};
let Some(output) = arguments.next() else {
eprintln!("usage: problem-catalog --output <file>");
return ExitCode::from(2);
};
if flag != "--output" || arguments.next().is_some() {
eprintln!("usage: problem-catalog --output <file>");
return ExitCode::from(2);
}

let catalog = ProblemCatalog {
entries: ProblemCode::ALL
.iter()
.copied()
.map(|problem| ProblemEntry {
uri: problem.type_uri(),
code: problem.code(),
title: problem.title(),
description: problem.detail(),
http_statuses: [problem.status()],
})
.collect(),
};
let mut bytes = match serde_json::to_vec_pretty(&catalog) {
Ok(bytes) => bytes,
Err(_) => {
eprintln!("problem catalog could not be serialized");
return ExitCode::FAILURE;
}
};
bytes.push(b'\n');
let output = PathBuf::from(output);
if fs::write(output, bytes).is_err() {
eprintln!("problem catalog could not be written");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
4 changes: 3 additions & 1 deletion crates/registry-relay-v2/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,9 @@ fn absolute(base: &str, path: &str) -> String {
format!("{}{path}", base.trim_end_matches('/'))
}

fn audit_event_schema() -> Value {
/// Return the fixed value-free audit event JSON Schema published in packages.
#[must_use]
pub fn audit_event_schema() -> Value {
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://id.registrystack.org/schemas/registry-relay/audit-event/v2alpha1",
Expand Down
Loading
Loading