Skip to content
Open
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
28 changes: 26 additions & 2 deletions .github/workflows/platform-schema-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,50 @@ name: platform-schema conformance

# Verify docs/platforms/schema/*.toml stays in sync with the code: structural
# schema validation + that every `source` code-ref still exists in the tree.
# Runs on any change to the schema files, the template, or the checker itself.
# Runs on any change to the schema files, the checker itself, or the adapter/core
# source trees that schema code-refs point into (so a rename/delete there is
# caught immediately, not by the next unrelated schema PR).
on:
pull_request:
paths:
- "docs/platforms/schema/**"
- "docs/platforms/_template.toml"
- "crates/platform-schema/**"
- "crates/openab-gateway/src/**"
- "crates/openab-core/src/**"
- "src/main.rs"
push:
branches: [main]
paths:
- "docs/platforms/schema/**"
- "docs/platforms/_template.toml"
- "crates/platform-schema/**"
- "crates/openab-gateway/src/**"
- "crates/openab-core/src/**"
- "src/main.rs"
schedule:
# Safety net: catch drift from renames/deletes even if a PR touching the
# source tree somehow bypassed the path triggers above (e.g. merged via
# a workflow that skipped this check).
- cron: "0 6 * * 1"
workflow_dispatch:

concurrency:
# event_name is included because a scheduled run and a push-to-main run
# both resolve github.ref to refs/heads/main; without it they'd share a
# group and cancel-in-progress could cancel one in favor of the other.
group: platform-schema-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13)
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 (2026-03-12)
with:
workspaces: "crates/platform-schema -> target"
# Standalone crate (serde + toml only); excluded from the root workspace
# so it builds without the heavy adapter crates.
# --locked: build strictly from the committed Cargo.lock; fail if it is
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ gh run view <run_id> --repo openabdev/openab --json conclusion -q .conclusion

When modifying a platform adapter (`crates/openab-gateway/src/adapters/*.rs`), check whether the change affects the platform's documented capabilities or feature status. If it does, update the corresponding `docs/platforms/schema/<platform>.toml`.

If you notice a platform's official API has changed (a new webhook event type, a deprecated field, etc.) or spot a platform-specific quirk while working on something else, feel free to update the corresponding `docs/platforms/schema/<platform>.toml` (`[capability.*]` or `[[quirks]]`) directly in your PR — no need to wait for a dedicated owner to notice.

See [`docs/platforms/README.md`](docs/platforms/README.md) for:
- The three-schema structure (capability, feature-support, quirks)
- How to add a new feature to the closed set
Expand Down
39 changes: 39 additions & 0 deletions crates/platform-schema/tests/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ fn check_code_ref(root: &Path, src: &str) -> Result<(), String> {
if !path.is_file() {
return Err(format!("source file {:?} does not exist", r.file));
}
let canonical_root = root.canonicalize().map_err(|e| e.to_string())?;
let canonical_path = path.canonicalize().map_err(|e| e.to_string())?;
if !canonical_path.starts_with(&canonical_root) {
return Err(format!("source path {:?} escapes repo root", r.file));
}
if let Some(sym) = r.symbol {
if sym.is_empty() {
return Err(format!("empty symbol after '#' in {:?}", r.file));
}
let text = fs::read_to_string(&path).map_err(|e| e.to_string())?;
if !text.contains(sym) {
return Err(format!("symbol {sym:?} not found in {:?} (renamed/deleted?)", r.file));
Expand All @@ -180,6 +188,37 @@ fn check_code_ref(root: &Path, src: &str) -> Result<(), String> {
Ok(())
}

#[test]
fn check_code_ref_rejects_empty_symbol() {
let root = repo_root();
let err = check_code_ref(&root, "docs/platforms/README.md#")
.expect_err("empty symbol must be rejected");
assert!(err.contains("empty symbol"), "unexpected error: {err}");
}

#[test]
fn check_code_ref_rejects_path_traversal() {
let root = repo_root();
let err = check_code_ref(&root, "../../../../../../../../../../etc/passwd")
.expect_err("path escaping repo root must be rejected");
assert!(
err.contains("does not exist") || err.contains("escapes repo root"),
"unexpected error: {err}"
);
}

#[test]
fn check_code_ref_rejects_absolute_path() {
// Path::join replaces the base entirely when the joined path is absolute,
// so this exercises a different code path than the "../" traversal above.
let root = repo_root();
let err = check_code_ref(&root, "/etc/passwd").expect_err("absolute path must be rejected");
assert!(
err.contains("does not exist") || err.contains("escapes repo root"),
"unexpected error: {err}"
);
}

/// The template must keep enumerating every capability section + feature key, so
/// a struct change can't silently leave the human-facing template behind.
#[test]
Expand Down
4 changes: 4 additions & 0 deletions docs/platforms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Each `schema/<platform>.toml` has three schema-driven parts:

**Sourcing rule:** attach the source that answers *"why should I trust or keep this?"* — intrinsic `(A)` facts link the **official platform doc** (a `source` URL); OpenAB `(B)` decisions/findings point at the **code** (`file.rs#symbol`) and, where relevant, the **PR** (`pr` / `refs`). Code refs use a grep-stable `#symbol` (no line numbers), so conformance can confirm they still exist without breaking on unrelated edits above the target.

> **Decision:** unlike `[[quirks]]`, every `[[openab_features]]` source must currently be a code-ref — `feature_sources_exist_in_tree` rejects URL sources rather than skipping them. This is intentional, not an oversight: no feature currently needs to cite an official-doc URL, and relaxing the check preemptively would let feature sources silently drift to unverifiable doc links instead of code. Revisit if/when a feature genuinely needs a URL source (see #1340).

## Conformance

`crates/platform-schema` deserializes every `schema/*.toml` into typed structs and, in CI, enforces:
Expand All @@ -24,6 +26,8 @@ Each `schema/<platform>.toml` has three schema-driven parts:

- **Current schema version: `2026-07-08`** — the top-line `schema_version` in each file. Bump it when the schema changes; the conformance test then flags every file that hasn't been re-verified.

**Known limitation:** conformance only checks that a code-ref's file/symbol still *exists* — it can't detect a symbol whose behavior changed without being renamed or removed, or a `note`/`status` that has quietly gone stale while the code-ref it cites remains technically valid. That kind of semantic drift is caught only by PR review (see `CONTRIBUTING.md`), not by CI. No dedicated per-platform owner is assigned to periodically audit for it; revisit if this proves insufficient in practice.

## Platforms

| Platform | Schema file |
Expand Down
Loading