Skip to content

Commit 558c342

Browse files
committed
feat: add authoritative identifier catalog
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 9b0e3f3 commit 558c342

23 files changed

Lines changed: 2526 additions & 168 deletions

File tree

‎.github/scripts/ci_changes.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,52 @@
170170
AUTHORING_REFERENCE_MANIFEST = (
171171
REPO_ROOT / "docs/site/scripts/authoring-reference-sources.json"
172172
)
173+
IDENTIFIER_CATALOG_CONTRACT = (
174+
REPO_ROOT / "products/identifiers/contracts/catalog-source.json"
175+
)
176+
177+
178+
def identifier_catalog_inputs(
179+
contract_path: Path = IDENTIFIER_CATALOG_CONTRACT,
180+
) -> tuple[str, ...]:
181+
"""Derive every source path that can change the public identifier catalog."""
182+
183+
contract = json.loads(contract_path.read_text(encoding="utf-8"))
184+
schema_groups = contract.get("schemaSources")
185+
records = contract.get("records")
186+
if not isinstance(schema_groups, list) or not isinstance(records, list):
187+
raise ValueError(
188+
f"identifier catalog has an invalid source contract: {contract_path}"
189+
)
190+
191+
inputs = [
192+
"products/identifiers/**",
193+
"crates/registry-relay-v2/src/problem.rs",
194+
]
195+
for index, group in enumerate(schema_groups):
196+
pattern = group.get("glob") if isinstance(group, dict) else None
197+
if not isinstance(pattern, str) or not pattern:
198+
raise ValueError(f"identifier schemaSources[{index}] has no glob")
199+
inputs.append(pattern)
200+
source = group.get("sourcePath")
201+
if source is not None:
202+
if not isinstance(source, str) or not source:
203+
raise ValueError(
204+
f"identifier schemaSources[{index}] has an invalid sourcePath"
205+
)
206+
inputs.append(source)
207+
for index, record in enumerate(records):
208+
source = record.get("sourcePath") if isinstance(record, dict) else None
209+
if not isinstance(source, str) or not source:
210+
raise ValueError(f"identifier records[{index}] has no sourcePath")
211+
inputs.append(source)
212+
213+
if any(source.startswith(("/", "../")) for source in inputs):
214+
raise ValueError("identifier catalog inputs must be repository-relative")
215+
return tuple(dict.fromkeys(inputs))
216+
217+
218+
IDENTIFIER_CATALOG_INPUTS = identifier_catalog_inputs()
173219

174220

175221
def authoring_reference_contract_sources(
@@ -405,6 +451,10 @@ def classify(
405451
seeds.update(PLATFORM_PACKAGES)
406452
elif path.startswith("products/relay-v2/"):
407453
seeds.update(RELAY_V2_PACKAGES)
454+
elif path.startswith("products/identifiers/"):
455+
# The catalog gate compiles its focused Relay V2 exporter.
456+
# Catalog-only tooling does not require the full Rust matrix.
457+
pass
408458
elif path in {
409459
"docs/site/src/data/generated/relay-support.json",
410460
"docs/site/src/data/relay-support.yaml",
@@ -421,6 +471,10 @@ def classify(
421471
)
422472
complete = run_all or force_all
423473

474+
identifiers = complete or any(
475+
matches(path, *IDENTIFIER_CATALOG_INPUTS) for path in paths
476+
)
477+
424478
platform = complete or any(
425479
matches(
426480
path,
@@ -644,6 +698,7 @@ def classify(
644698
"client_bindings": client_bindings,
645699
"registryctl_tutorial": registryctl_tutorial,
646700
"evidence_tutorial": evidence_tutorial,
701+
"identifiers": identifiers,
647702
}
648703

649704

‎.github/scripts/test_ci_changes.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AUTHORING_REFERENCE_INPUTS,
1818
EVIDENCE_AUTHORING_GUIDE_IMPLEMENTATION_INPUTS,
1919
EVIDENCE_TUTORIAL_INPUTS,
20+
IDENTIFIER_CATALOG_INPUTS,
2021
RELEASE_SECURITY_WORKFLOWS,
2122
SHARDS,
2223
Workspace,
@@ -232,6 +233,27 @@ def test_relay_v2_paths_select_the_editor_and_reverse_dependents(self) -> None:
232233
]:
233234
self.assertIn(package, outputs["rust_packages"])
234235

236+
def test_every_identifier_source_selects_the_catalog_gate(self) -> None:
237+
for pattern in IDENTIFIER_CATALOG_INPUTS:
238+
sample = pattern.replace("**", "sample").replace("*", "sample")
239+
with self.subTest(pattern=pattern):
240+
self.assertTrue(classify(self.workspace, (sample,))["identifiers"])
241+
242+
def test_ci_always_checks_repository_identifier_reference_closure(self) -> None:
243+
workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
244+
self.assertIn(
245+
"products/identifiers/scripts/generate.py --check-references",
246+
workflow,
247+
)
248+
249+
def test_identifier_tooling_does_not_force_the_rust_matrix(self) -> None:
250+
outputs = classify(
251+
self.workspace,
252+
("products/identifiers/scripts/generate.py",),
253+
)
254+
self.assertTrue(outputs["identifiers"])
255+
self.assertFalse(outputs["rust"])
256+
235257
def test_relay_v2_product_material_selects_runtime_and_tooling(self) -> None:
236258
outputs = classify(
237259
self.workspace,

‎.github/workflows/ci.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
client_bindings: ${{ steps.filter.outputs.client_bindings }}
5656
registryctl_tutorial: ${{ steps.filter.outputs.registryctl_tutorial }}
5757
evidence_tutorial: ${{ steps.filter.outputs.evidence_tutorial }}
58+
identifiers: ${{ steps.filter.outputs.identifiers }}
5859
steps:
5960
- name: Checkout
6061
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -99,6 +100,9 @@ jobs:
99100
fi
100101
"${classifier[@]}"
101102
103+
- name: Check repository identifier reference closure
104+
run: products/identifiers/scripts/generate.py --check-references
105+
102106
- name: Test CI classifier
103107
run: python3 .github/scripts/test_ci_changes.py
104108

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

574+
identifiers:
575+
name: Public identifier catalog
576+
needs: changes
577+
if: needs.changes.outputs.identifiers == 'true'
578+
runs-on: ubuntu-24.04
579+
timeout-minutes: 10
580+
steps:
581+
- name: Checkout
582+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
583+
with:
584+
persist-credentials: false
585+
submodules: false
586+
587+
- name: Cache Cargo registry
588+
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
589+
with:
590+
shared-key: workspace-registry
591+
cache-targets: false
592+
save-if: ${{ github.ref == 'refs/heads/main' }}
593+
594+
- name: Check public identifier catalog
595+
run: products/identifiers/scripts/check.sh
596+
570597
rust-result:
571598
name: Rust workspace
572599
if: always()
@@ -578,6 +605,7 @@ jobs:
578605
- evidence-contracts
579606
- relay-contracts
580607
- relay-v2-contracts
608+
- identifiers
581609
runs-on: ubuntu-24.04
582610
env:
583611
RUST_JOB_RESULTS: ${{ toJSON(needs) }}

‎crates/registry-manifest-core/src/lib.rs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ const BUILTIN_VOCABULARIES: &[(&str, &str)] = &[
7474
"registry_manifest",
7575
"https://id.registrystack.org/ns/registry-manifest/v1#",
7676
),
77-
(
78-
"registry_relay",
79-
"https://id.registrystack.org/ns/registry-relay/v1#",
80-
),
8177
("sh", "http://www.w3.org/ns/shacl#"),
8278
("skos", "http://www.w3.org/2004/02/skos/core#"),
8379
("xsd", "http://www.w3.org/2001/XMLSchema#"),

‎crates/registry-manifest-core/tests/metadata_core.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -984,10 +984,10 @@ fn vocabularies_protect_builtins_and_validate_custom_namespaces() {
984984
);
985985
manifest.vocabularies.insert(
986986
"registry_relay".to_string(),
987-
"https://id.registrystack.org/ns/registry-relay/v1#".to_string(),
987+
"https://example.org/relay/ns#".to_string(),
988988
);
989989
validate_manifest(&manifest)
990-
.expect("safe custom vocabulary and identical protected values pass");
990+
.expect("safe custom vocabularies and identical protected values pass");
991991
}
992992

993993
#[test]
@@ -2044,7 +2044,7 @@ datasets:
20442044
lookup_keys: [national_id]
20452045
access:
20462046
kind: evidence-server
2047-
conforms_to: registry_relay:evidence-server-v1
2047+
conforms_to: https://example.test/evidence-server/v1
20482048
endpoint_url: https://evidence.example.test
20492049
discovery_url: https://evidence.example.test/.well-known/evidence-service
20502050
ruleset: smallholder-v1
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use std::{env, fs, path::PathBuf, process::ExitCode};
4+
5+
use registry_relay_v2::artifacts::audit_event_schema;
6+
7+
fn main() -> ExitCode {
8+
let mut arguments = env::args_os().skip(1);
9+
let Some(flag) = arguments.next() else {
10+
eprintln!("usage: audit-event-schema --output <file>");
11+
return ExitCode::from(2);
12+
};
13+
let Some(output) = arguments.next() else {
14+
eprintln!("usage: audit-event-schema --output <file>");
15+
return ExitCode::from(2);
16+
};
17+
if flag != "--output" || arguments.next().is_some() {
18+
eprintln!("usage: audit-event-schema --output <file>");
19+
return ExitCode::from(2);
20+
}
21+
22+
let mut bytes = match serde_json::to_vec_pretty(&audit_event_schema()) {
23+
Ok(bytes) => bytes,
24+
Err(_) => {
25+
eprintln!("audit event schema could not be serialized");
26+
return ExitCode::FAILURE;
27+
}
28+
};
29+
bytes.push(b'\n');
30+
if fs::write(PathBuf::from(output), bytes).is_err() {
31+
eprintln!("audit event schema could not be written");
32+
return ExitCode::FAILURE;
33+
}
34+
ExitCode::SUCCESS
35+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use std::{env, fs, path::PathBuf, process::ExitCode};
4+
5+
use registry_relay_v2::problem::ProblemCode;
6+
use serde::Serialize;
7+
8+
#[derive(Serialize)]
9+
#[serde(rename_all = "camelCase")]
10+
struct ProblemCatalog<'a> {
11+
entries: Vec<ProblemEntry<'a>>,
12+
}
13+
14+
#[derive(Serialize)]
15+
#[serde(rename_all = "camelCase")]
16+
struct ProblemEntry<'a> {
17+
uri: String,
18+
code: &'a str,
19+
title: &'a str,
20+
description: &'a str,
21+
http_statuses: [u16; 1],
22+
}
23+
24+
fn main() -> ExitCode {
25+
let mut arguments = env::args_os().skip(1);
26+
let Some(flag) = arguments.next() else {
27+
eprintln!("usage: problem-catalog --output <file>");
28+
return ExitCode::from(2);
29+
};
30+
let Some(output) = arguments.next() else {
31+
eprintln!("usage: problem-catalog --output <file>");
32+
return ExitCode::from(2);
33+
};
34+
if flag != "--output" || arguments.next().is_some() {
35+
eprintln!("usage: problem-catalog --output <file>");
36+
return ExitCode::from(2);
37+
}
38+
39+
let catalog = ProblemCatalog {
40+
entries: ProblemCode::ALL
41+
.iter()
42+
.copied()
43+
.map(|problem| ProblemEntry {
44+
uri: problem.type_uri(),
45+
code: problem.code(),
46+
title: problem.title(),
47+
description: problem.detail(),
48+
http_statuses: [problem.status()],
49+
})
50+
.collect(),
51+
};
52+
let mut bytes = match serde_json::to_vec_pretty(&catalog) {
53+
Ok(bytes) => bytes,
54+
Err(_) => {
55+
eprintln!("problem catalog could not be serialized");
56+
return ExitCode::FAILURE;
57+
}
58+
};
59+
bytes.push(b'\n');
60+
let output = PathBuf::from(output);
61+
if fs::write(output, bytes).is_err() {
62+
eprintln!("problem catalog could not be written");
63+
return ExitCode::FAILURE;
64+
}
65+
ExitCode::SUCCESS
66+
}

‎crates/registry-relay-v2/src/artifacts.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1683,7 +1683,9 @@ fn absolute(base: &str, path: &str) -> String {
16831683
format!("{}{path}", base.trim_end_matches('/'))
16841684
}
16851685

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

0 commit comments

Comments
 (0)