From 228032a4ace7b8027dc5efb2ea2cd59995a3e3b0 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 14:26:31 +0200 Subject: [PATCH 01/10] Add static OpenAPI spec and Stainless SDK config Introduce SDK generation scaffolding: commit a static openapi.json extracted from the Utoipa annotations via a golden-file test, add Stainless workspace/config for TypeScript and Python SDKs, and clean up operation IDs for ergonomic generated method names. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + .stainless/stainless.yml | 60 + .stainless/workspace.json | 13 + crates/omnigraph-server/src/lib.rs | 17 + crates/omnigraph-server/tests/openapi.rs | 25 + openapi.json | 1771 ++++++++++++++++++++++ 6 files changed, 1887 insertions(+) create mode 100644 .stainless/stainless.yml create mode 100644 .stainless/workspace.json create mode 100644 openapi.json diff --git a/.gitignore b/.gitignore index 6f70bdc..ee09166 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__/ *.pyc demo/*.omni/ .omnigraph-rustfs-demo/ +sdks/ diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml new file mode 100644 index 0000000..d13f0c4 --- /dev/null +++ b/.stainless/stainless.yml @@ -0,0 +1,60 @@ +edition: "2026-02-23" + +organization: + name: omnigraph + docs: https://github.com/ModernRelay/omnigraph + github_org: ModernRelay + +targets: + typescript: + package_name: omnigraph + production_repo: ModernRelay/omnigraph-typescript + publish: + npm: true + python: + package_name: omnigraph + production_repo: ModernRelay/omnigraph-python + publish: + pypi: true + +client_settings: + opts: + api_key: + type: string + auth: + security_scheme: bearer_token + base_url: + type: string + +resources: + $client: + methods: + read: post /read + change: post /change + export: post /export + ingest: post /ingest + schema: + methods: + apply: post /schema/apply + branches: + methods: + list: get /branches + create: post /branches + delete: delete /branches/{branch} + merge: post /branches/merge + runs: + methods: + list: get /runs + retrieve: get /runs/{run_id} + publish: post /runs/{run_id}/publish + abort: post /runs/{run_id}/abort + commits: + methods: + list: get /commits + retrieve: get /commits/{commit_id} + snapshots: + methods: + retrieve: get /snapshot + +settings: + license: MIT diff --git a/.stainless/workspace.json b/.stainless/workspace.json new file mode 100644 index 0000000..924141f --- /dev/null +++ b/.stainless/workspace.json @@ -0,0 +1,13 @@ +{ + "project": "omnigraph", + "openapi_spec": "../openapi.json", + "stainless_config": "./stainless.yml", + "targets": { + "typescript": { + "output_path": "../sdks/omnigraph-typescript" + }, + "python": { + "output_path": "../sdks/omnigraph-python" + } + } +} diff --git a/crates/omnigraph-server/src/lib.rs b/crates/omnigraph-server/src/lib.rs index e8d0e7d..0e701ae 100644 --- a/crates/omnigraph-server/src/lib.rs +++ b/crates/omnigraph-server/src/lib.rs @@ -465,6 +465,7 @@ async fn shutdown_signal() { get, path = "/healthz", tag = "health", + operation_id = "health", responses( (status = 200, description = "Server is healthy", body = HealthOutput), ), @@ -575,6 +576,7 @@ fn authorize_request( get, path = "/snapshot", tag = "snapshots", + operation_id = "getSnapshot", params(SnapshotQuery), responses( (status = 200, description = "Database snapshot", body = api::SnapshotOutput), @@ -615,6 +617,7 @@ async fn server_snapshot( post, path = "/read", tag = "queries", + operation_id = "read", request_body = ReadRequest, responses( (status = 200, description = "Query results", body = ReadOutput), @@ -684,6 +687,7 @@ async fn server_read( post, path = "/export", tag = "queries", + operation_id = "export", request_body = ExportRequest, responses( (status = 200, description = "Exported data as NDJSON", content_type = "application/x-ndjson"), @@ -742,6 +746,7 @@ async fn server_export( post, path = "/change", tag = "mutations", + operation_id = "change", request_body = ChangeRequest, responses( (status = 200, description = "Mutation results", body = ChangeOutput), @@ -800,6 +805,7 @@ async fn server_change( post, path = "/schema/apply", tag = "mutations", + operation_id = "applySchema", request_body = SchemaApplyRequest, responses( (status = 200, description = "Schema apply results", body = SchemaApplyOutput), @@ -838,6 +844,7 @@ async fn server_schema_apply( post, path = "/ingest", tag = "mutations", + operation_id = "ingest", request_body = IngestRequest, responses( (status = 200, description = "Ingest results", body = IngestOutput), @@ -907,6 +914,7 @@ async fn server_ingest( get, path = "/branches", tag = "branches", + operation_id = "listBranches", responses( (status = 200, description = "List of branches", body = BranchListOutput), (status = 401, description = "Unauthorized", body = ErrorOutput), @@ -943,6 +951,7 @@ async fn server_branch_list( post, path = "/branches", tag = "branches", + operation_id = "createBranch", request_body = BranchCreateRequest, responses( (status = 200, description = "Branch created", body = BranchCreateOutput), @@ -990,6 +999,7 @@ async fn server_branch_create( delete, path = "/branches/{branch}", tag = "branches", + operation_id = "deleteBranch", params( ("branch" = String, Path, description = "Branch name to delete"), ), @@ -1034,6 +1044,7 @@ async fn server_branch_delete( post, path = "/branches/merge", tag = "branches", + operation_id = "mergeBranches", request_body = BranchMergeRequest, responses( (status = 200, description = "Branches merged", body = BranchMergeOutput), @@ -1079,6 +1090,7 @@ async fn server_branch_merge( get, path = "/runs", tag = "runs", + operation_id = "listRuns", responses( (status = 200, description = "List of runs", body = RunListOutput), (status = 401, description = "Unauthorized", body = ErrorOutput), @@ -1116,6 +1128,7 @@ async fn server_run_list( get, path = "/runs/{run_id}", tag = "runs", + operation_id = "getRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1158,6 +1171,7 @@ async fn server_run_show( post, path = "/runs/{run_id}/publish", tag = "runs", + operation_id = "publishRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1207,6 +1221,7 @@ async fn server_run_publish( post, path = "/runs/{run_id}/abort", tag = "runs", + operation_id = "abortRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1255,6 +1270,7 @@ async fn server_run_abort( get, path = "/commits", tag = "commits", + operation_id = "listCommits", params(CommitListQuery), responses( (status = 200, description = "List of commits", body = CommitListOutput), @@ -1296,6 +1312,7 @@ async fn server_commit_list( get, path = "/commits/{commit_id}", tag = "commits", + operation_id = "getCommit", params( ("commit_id" = String, Path, description = "Commit identifier"), ), diff --git a/crates/omnigraph-server/tests/openapi.rs b/crates/omnigraph-server/tests/openapi.rs index f47ccdf..22f3580 100644 --- a/crates/omnigraph-server/tests/openapi.rs +++ b/crates/omnigraph-server/tests/openapi.rs @@ -962,3 +962,28 @@ async fn auth_mode_healthz_still_has_no_security() { "auth-mode: /healthz should still have no security" ); } + +#[test] +fn openapi_spec_is_up_to_date() { + let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../openapi.json"); + + let generated = serde_json::to_string_pretty(&openapi_doc()).unwrap() + "\n"; + + if env::var("OMNIGRAPH_UPDATE_OPENAPI").is_ok() { + fs::write(&spec_path, &generated).unwrap(); + return; + } + + let committed = fs::read_to_string(&spec_path).unwrap_or_else(|_| { + panic!( + "openapi.json not found at {}. Run: OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date", + spec_path.display() + ) + }); + + assert_eq!( + committed, generated, + "openapi.json is out of date. Run: OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date" + ); +} diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..b341c0c --- /dev/null +++ b/openapi.json @@ -0,0 +1,1771 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Omnigraph API", + "description": "HTTP API for the Omnigraph graph database", + "license": { + "name": "MIT", + "identifier": "MIT" + }, + "version": "0.2.2" + }, + "paths": { + "/branches": { + "get": { + "tags": [ + "branches" + ], + "operationId": "listBranches", + "responses": { + "200": { + "description": "List of branches", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + }, + "post": { + "tags": [ + "branches" + ], + "operationId": "createBranch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Branch created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchCreateOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Branch already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/branches/merge": { + "post": { + "tags": [ + "branches" + ], + "operationId": "mergeBranches", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchMergeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Branches merged", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchMergeOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Merge conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/branches/{branch}": { + "delete": { + "tags": [ + "branches" + ], + "operationId": "deleteBranch", + "parameters": [ + { + "name": "branch", + "in": "path", + "description": "Branch name to delete", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Branch deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchDeleteOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Branch not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/change": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "change", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Mutation results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Merge conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/commits": { + "get": { + "tags": [ + "commits" + ], + "operationId": "listCommits", + "parameters": [ + { + "name": "branch", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { + "200": { + "description": "List of commits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/commits/{commit_id}": { + "get": { + "tags": [ + "commits" + ], + "operationId": "getCommit", + "parameters": [ + { + "name": "commit_id", + "in": "path", + "description": "Commit identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Commit details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Commit not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/export": { + "post": { + "tags": [ + "queries" + ], + "operationId": "export", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Exported data as NDJSON", + "content": { + "application/x-ndjson": {} + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/healthz": { + "get": { + "tags": [ + "health" + ], + "operationId": "health", + "responses": { + "200": { + "description": "Server is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthOutput" + } + } + } + } + } + } + }, + "/ingest": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "ingest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Ingest results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/read": { + "post": { + "tags": [ + "queries" + ], + "operationId": "read", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Query results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs": { + "get": { + "tags": [ + "runs" + ], + "operationId": "listRuns", + "responses": { + "200": { + "description": "List of runs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}": { + "get": { + "tags": [ + "runs" + ], + "operationId": "getRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}/abort": { + "post": { + "tags": [ + "runs" + ], + "operationId": "abortRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run aborted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}/publish": { + "post": { + "tags": [ + "runs" + ], + "operationId": "publishRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run published", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/schema/apply": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "applySchema", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaApplyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Schema apply results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaApplyOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/snapshot": { + "get": { + "tags": [ + "snapshots" + ], + "operationId": "getSnapshot", + "parameters": [ + { + "name": "branch", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { + "200": { + "description": "Database snapshot", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + } + }, + "components": { + "schemas": { + "BranchCreateOutput": { + "type": "object", + "required": [ + "uri", + "from", + "name" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "BranchCreateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "from": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + } + } + }, + "BranchDeleteOutput": { + "type": "object", + "required": [ + "uri", + "name" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "BranchListOutput": { + "type": "object", + "required": [ + "branches" + ], + "properties": { + "branches": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BranchMergeOutcome": { + "type": "string", + "enum": [ + "already_up_to_date", + "fast_forward", + "merged" + ] + }, + "BranchMergeOutput": { + "type": "object", + "required": [ + "source", + "target", + "outcome" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "outcome": { + "$ref": "#/components/schemas/BranchMergeOutcome" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + } + } + }, + "BranchMergeRequest": { + "type": "object", + "required": [ + "source" + ], + "properties": { + "source": { + "type": "string" + }, + "target": { + "type": [ + "string", + "null" + ] + } + } + }, + "ChangeOutput": { + "type": "object", + "required": [ + "branch", + "query_name", + "affected_nodes", + "affected_edges" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "affected_edges": { + "type": "integer", + "minimum": 0 + }, + "affected_nodes": { + "type": "integer", + "minimum": 0 + }, + "branch": { + "type": "string" + }, + "query_name": { + "type": "string" + } + } + }, + "ChangeRequest": { + "type": "object", + "required": [ + "query_source" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "params": {}, + "query_name": { + "type": [ + "string", + "null" + ] + }, + "query_source": { + "type": "string" + } + } + }, + "CommitListOutput": { + "type": "object", + "required": [ + "commits" + ], + "properties": { + "commits": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommitOutput" + } + } + } + }, + "CommitOutput": { + "type": "object", + "required": [ + "graph_commit_id", + "manifest_version", + "created_at" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "graph_commit_id": { + "type": "string" + }, + "manifest_branch": { + "type": [ + "string", + "null" + ] + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "merged_parent_commit_id": { + "type": [ + "string", + "null" + ] + }, + "parent_commit_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "ErrorCode": { + "type": "string", + "enum": [ + "unauthorized", + "forbidden", + "bad_request", + "not_found", + "conflict", + "internal" + ] + }, + "ErrorOutput": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "code": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ErrorCode" + } + ] + }, + "error": { + "type": "string" + }, + "merge_conflicts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MergeConflictOutput" + } + } + } + }, + "ExportRequest": { + "type": "object", + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "table_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "type_names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "HealthOutput": { + "type": "object", + "required": [ + "status", + "version" + ], + "properties": { + "source_version": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "IngestOutput": { + "type": "object", + "required": [ + "uri", + "branch", + "base_branch", + "branch_created", + "mode", + "tables" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "base_branch": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "branch_created": { + "type": "boolean" + }, + "mode": { + "$ref": "#/components/schemas/LoadMode" + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IngestTableOutput" + } + }, + "uri": { + "type": "string" + } + } + }, + "IngestRequest": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "string" + }, + "from": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/LoadMode" + } + ] + } + } + }, + "IngestTableOutput": { + "type": "object", + "required": [ + "table_key", + "rows_loaded" + ], + "properties": { + "rows_loaded": { + "type": "integer", + "minimum": 0 + }, + "table_key": { + "type": "string" + } + } + }, + "LoadMode": { + "type": "string", + "description": "Shadow enum for documenting [`LoadMode`] in the OpenAPI schema.", + "enum": [ + "overwrite", + "append", + "merge" + ] + }, + "MergeConflictKindOutput": { + "type": "string", + "enum": [ + "divergent_insert", + "divergent_update", + "delete_vs_update", + "orphan_edge", + "unique_violation", + "cardinality_violation", + "value_constraint_violation" + ] + }, + "MergeConflictOutput": { + "type": "object", + "required": [ + "table_key", + "kind", + "message" + ], + "properties": { + "kind": { + "$ref": "#/components/schemas/MergeConflictKindOutput" + }, + "message": { + "type": "string" + }, + "row_id": { + "type": [ + "string", + "null" + ] + }, + "table_key": { + "type": "string" + } + } + }, + "ReadOutput": { + "type": "object", + "required": [ + "query_name", + "target", + "row_count", + "rows" + ], + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "query_name": { + "type": "string" + }, + "row_count": { + "type": "integer", + "minimum": 0 + }, + "rows": {}, + "target": { + "$ref": "#/components/schemas/ReadTargetOutput" + } + } + }, + "ReadRequest": { + "type": "object", + "required": [ + "query_source" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "params": {}, + "query_name": { + "type": [ + "string", + "null" + ] + }, + "query_source": { + "type": "string" + }, + "snapshot": { + "type": [ + "string", + "null" + ] + } + } + }, + "ReadTargetOutput": { + "type": "object", + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "snapshot": { + "type": [ + "string", + "null" + ] + } + } + }, + "RunListOutput": { + "type": "object", + "required": [ + "runs" + ], + "properties": { + "runs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "RunOutput": { + "type": "object", + "required": [ + "run_id", + "target_branch", + "run_branch", + "base_snapshot_id", + "base_manifest_version", + "status", + "created_at", + "updated_at" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "base_manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "base_snapshot_id": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "operation_hash": { + "type": [ + "string", + "null" + ] + }, + "published_snapshot_id": { + "type": [ + "string", + "null" + ] + }, + "run_branch": { + "type": "string" + }, + "run_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "target_branch": { + "type": "string" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + } + }, + "SchemaApplyOutput": { + "type": "object", + "required": [ + "uri", + "supported", + "applied", + "step_count", + "manifest_version", + "steps" + ], + "properties": { + "applied": { + "type": "boolean" + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "step_count": { + "type": "integer", + "minimum": 0 + }, + "steps": { + "type": "array", + "items": {} + }, + "supported": { + "type": "boolean" + }, + "uri": { + "type": "string" + } + } + }, + "SchemaApplyRequest": { + "type": "object", + "required": [ + "schema_source" + ], + "properties": { + "schema_source": { + "type": "string" + } + } + }, + "SnapshotOutput": { + "type": "object", + "required": [ + "branch", + "manifest_version", + "tables" + ], + "properties": { + "branch": { + "type": "string" + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotTableOutput" + } + } + } + }, + "SnapshotTableOutput": { + "type": "object", + "required": [ + "table_key", + "table_path", + "table_version", + "row_count" + ], + "properties": { + "row_count": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "table_branch": { + "type": [ + "string", + "null" + ] + }, + "table_key": { + "type": "string" + }, + "table_path": { + "type": "string" + }, + "table_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + } + }, + "securitySchemes": { + "bearer_token": { + "type": "http", + "scheme": "bearer" + } + } + } +} From bfdfeaa2f2376d7dd531451b1e319a5dc1d611a0 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 14:31:35 +0200 Subject: [PATCH 02/10] Remove Stainless SDK config Drop .stainless/ workspace and generator config; keep the static openapi.json, golden-file test, and operation_id cleanup so the spec stays usable with any SDK generator. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 - .stainless/stainless.yml | 60 --------------------------------------- .stainless/workspace.json | 13 --------- 3 files changed, 74 deletions(-) delete mode 100644 .stainless/stainless.yml delete mode 100644 .stainless/workspace.json diff --git a/.gitignore b/.gitignore index ee09166..6f70bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ __pycache__/ *.pyc demo/*.omni/ .omnigraph-rustfs-demo/ -sdks/ diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml deleted file mode 100644 index d13f0c4..0000000 --- a/.stainless/stainless.yml +++ /dev/null @@ -1,60 +0,0 @@ -edition: "2026-02-23" - -organization: - name: omnigraph - docs: https://github.com/ModernRelay/omnigraph - github_org: ModernRelay - -targets: - typescript: - package_name: omnigraph - production_repo: ModernRelay/omnigraph-typescript - publish: - npm: true - python: - package_name: omnigraph - production_repo: ModernRelay/omnigraph-python - publish: - pypi: true - -client_settings: - opts: - api_key: - type: string - auth: - security_scheme: bearer_token - base_url: - type: string - -resources: - $client: - methods: - read: post /read - change: post /change - export: post /export - ingest: post /ingest - schema: - methods: - apply: post /schema/apply - branches: - methods: - list: get /branches - create: post /branches - delete: delete /branches/{branch} - merge: post /branches/merge - runs: - methods: - list: get /runs - retrieve: get /runs/{run_id} - publish: post /runs/{run_id}/publish - abort: post /runs/{run_id}/abort - commits: - methods: - list: get /commits - retrieve: get /commits/{commit_id} - snapshots: - methods: - retrieve: get /snapshot - -settings: - license: MIT diff --git a/.stainless/workspace.json b/.stainless/workspace.json deleted file mode 100644 index 924141f..0000000 --- a/.stainless/workspace.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "project": "omnigraph", - "openapi_spec": "../openapi.json", - "stainless_config": "./stainless.yml", - "targets": { - "typescript": { - "output_path": "../sdks/omnigraph-typescript" - }, - "python": { - "output_path": "../sdks/omnigraph-python" - } - } -} From 7c6d89d194ab2c682ca8267e709b7cd6dafb5813 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 15:53:13 +0200 Subject: [PATCH 03/10] Add pre-commit hook for openapi.json drift Wire a local pre-commit hook that regenerates openapi.json whenever the server source changes, and document the workflow in CONTRIBUTING. Opt-in via `pre-commit install`; the existing CI test remains the authoritative drift check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .pre-commit-config.yaml | 10 ++++++++++ CONTRIBUTING.md | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cfe2e89 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: local + hooks: + - id: openapi-sync + name: Sync openapi.json + description: Regenerate openapi.json when the server API surface changes. + entry: bash -c 'OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date --quiet' + language: system + pass_filenames: false + files: ^(crates/omnigraph-server/src/.*\.rs|crates/omnigraph-server/Cargo\.toml|openapi\.json)$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65d1e24..eb6ce7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,24 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. +### OpenAPI spec + +`openapi.json` is a committed artifact generated from the Utoipa annotations in +`crates/omnigraph-server`. CI fails if it drifts from the source. To regenerate +manually: + +```bash +OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date +``` + +Optional: install [pre-commit](https://pre-commit.com) to run this automatically +before each commit that touches the server: + +```bash +pip install pre-commit +pre-commit install +``` + ## Pull Requests - keep changes focused From 2fedcf7e2f84debec244fdee7d12dd37fbdc772e Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 16:06:54 +0200 Subject: [PATCH 04/10] Revert "Add pre-commit hook for openapi.json drift" This reverts commit 7c6d89d194ab2c682ca8267e709b7cd6dafb5813. --- .pre-commit-config.yaml | 10 ---------- CONTRIBUTING.md | 18 ------------------ 2 files changed, 28 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index cfe2e89..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -repos: - - repo: local - hooks: - - id: openapi-sync - name: Sync openapi.json - description: Regenerate openapi.json when the server API surface changes. - entry: bash -c 'OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date --quiet' - language: system - pass_filenames: false - files: ^(crates/omnigraph-server/src/.*\.rs|crates/omnigraph-server/Cargo\.toml|openapi\.json)$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb6ce7c..65d1e24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,24 +16,6 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. -### OpenAPI spec - -`openapi.json` is a committed artifact generated from the Utoipa annotations in -`crates/omnigraph-server`. CI fails if it drifts from the source. To regenerate -manually: - -```bash -OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date -``` - -Optional: install [pre-commit](https://pre-commit.com) to run this automatically -before each commit that touches the server: - -```bash -pip install pre-commit -pre-commit install -``` - ## Pull Requests - keep changes focused From 7427d87e9a9106ad7c8d261d00439accb334356f Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 16:19:08 +0200 Subject: [PATCH 05/10] Add opt-in git hook for openapi.json drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track a project pre-commit hook under scripts/hooks/ that regenerates openapi.json when server source is staged, and auto-stages the updated spec into the commit. Zero external dependencies — plain bash + cargo. Enable via `git config core.hooksPath scripts/hooks`. The CI drift test remains the authoritative check. Co-Authored-By: Claude Opus 4.7 (1M context) --- CONTRIBUTING.md | 17 +++++++++++++++++ scripts/hooks/pre-commit | 15 +++++++++++++++ 2 files changed, 32 insertions(+) create mode 100755 scripts/hooks/pre-commit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65d1e24..45d2f1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,23 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. +### OpenAPI spec + +`openapi.json` is a committed artifact generated from the Utoipa annotations in +`crates/omnigraph-server`. CI fails if it drifts from the source. To regenerate +manually: + +```bash +OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date +``` + +Optional: enable the project's git hook to regenerate automatically when you +commit server changes: + +```bash +git config core.hooksPath scripts/hooks +``` + ## Pull Requests - keep changes focused diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..6fe8d8f --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Regenerate openapi.json when the server API surface changes. +set -euo pipefail + +staged=$(git diff --cached --name-only --diff-filter=ACMR) + +echo "$staged" | grep -qE '^(crates/omnigraph-server/src/.*\.rs|crates/omnigraph-server/Cargo\.toml)$' || exit 0 + +echo "[pre-commit] regenerating openapi.json..." +OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date --quiet + +if ! git diff --quiet -- openapi.json; then + git add openapi.json + echo "[pre-commit] openapi.json updated and staged" +fi From 9eb3c9fde4728f60dbd95da67518765b53addf33 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 16:26:57 +0200 Subject: [PATCH 06/10] Revert "Add opt-in git hook for openapi.json drift" This reverts commit 7427d87e9a9106ad7c8d261d00439accb334356f. --- CONTRIBUTING.md | 17 ----------------- scripts/hooks/pre-commit | 15 --------------- 2 files changed, 32 deletions(-) delete mode 100755 scripts/hooks/pre-commit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45d2f1f..65d1e24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,23 +16,6 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. -### OpenAPI spec - -`openapi.json` is a committed artifact generated from the Utoipa annotations in -`crates/omnigraph-server`. CI fails if it drifts from the source. To regenerate -manually: - -```bash -OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date -``` - -Optional: enable the project's git hook to regenerate automatically when you -commit server changes: - -```bash -git config core.hooksPath scripts/hooks -``` - ## Pull Requests - keep changes focused diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit deleted file mode 100755 index 6fe8d8f..0000000 --- a/scripts/hooks/pre-commit +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# Regenerate openapi.json when the server API surface changes. -set -euo pipefail - -staged=$(git diff --cached --name-only --diff-filter=ACMR) - -echo "$staged" | grep -qE '^(crates/omnigraph-server/src/.*\.rs|crates/omnigraph-server/Cargo\.toml)$' || exit 0 - -echo "[pre-commit] regenerating openapi.json..." -OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date --quiet - -if ! git diff --quiet -- openapi.json; then - git add openapi.json - echo "[pre-commit] openapi.json updated and staged" -fi From e0d48ad8254470deeb86661356b47c74eb63c0c2 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 16:28:01 +0200 Subject: [PATCH 07/10] Document openapi.json auto-sync in CI Describe the CI workflow that regenerates openapi.json on PRs and the fork fallback. The workflow itself is added in a follow-up commit via the GitHub API (local tooling lacks workflow-scope). Co-Authored-By: Claude Opus 4.7 (1M context) --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65d1e24..fcd67ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,21 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. +### OpenAPI spec + +`openapi.json` is a committed artifact generated from the Utoipa annotations in +`crates/omnigraph-server`. For PRs opened from this repository, a CI job +regenerates it automatically and commits the updated file back to the PR +branch. For PRs from forks (where CI cannot push), run the regeneration +manually: + +```bash +OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date +``` + +The workspace test run fails if the committed `openapi.json` drifts from what +the source generates. + ## Pull Requests - keep changes focused From dda9728473e070258c8e62b915d77b1424f92d16 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 17 Apr 2026 19:09:36 +0200 Subject: [PATCH 08/10] Add openapi.json auto-sync workflow --- .github/workflows/openapi-sync.yml | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/openapi-sync.yml diff --git a/.github/workflows/openapi-sync.yml b/.github/workflows/openapi-sync.yml new file mode 100644 index 0000000..52bf882 --- /dev/null +++ b/.github/workflows/openapi-sync.yml @@ -0,0 +1,60 @@ +name: Sync OpenAPI + +on: + pull_request: + paths: + - "crates/omnigraph-server/src/**" + - "crates/omnigraph-server/Cargo.toml" + - "crates/omnigraph-server/tests/openapi.rs" + +concurrency: + group: openapi-sync-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + sync: + name: Regenerate openapi.json + # Auto-commit only on same-repo PRs; forks cannot be pushed to. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout PR branch + uses: actions/checkout@v5.0.1 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler libprotobuf-dev + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build data + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . -> target + + - name: Regenerate openapi.json + run: | + OMNIGRAPH_UPDATE_OPENAPI=1 \ + cargo test --locked -p omnigraph-server --test openapi openapi_spec_is_up_to_date + + - name: Commit if changed + run: | + if git diff --quiet -- openapi.json; then + echo "openapi.json is already in sync." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add openapi.json + git commit -m "chore: regenerate openapi.json" + git push From a157f6a17c671b41f43d59fbe1fd274389db6a95 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Sat, 18 Apr 2026 21:00:46 +0200 Subject: [PATCH 09/10] Fold openapi.json auto-sync into main CI test job The separate openapi-sync workflow was duplicating the workspace build (~15 min cold-cache compile), paying the cost twice per PR. Fold the regen + auto-commit into the existing test job: one compile, shared rust-cache, same drift-check semantics. - Same-repo PRs: OMNIGRAPH_UPDATE_OPENAPI=1 during the test run, then commit the regenerated spec back to the PR branch - Fork PRs / pushes: env var empty, test stays in strict drift-check mode - openapi_spec_is_up_to_date treats empty env value as unset, so the conditional workflow env expression works Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 41 ++++++++++++++-- .github/workflows/openapi-sync.yml | 60 ------------------------ crates/omnigraph-server/tests/openapi.rs | 5 +- 3 files changed, 42 insertions(+), 64 deletions(-) delete mode 100644 .github/workflows/openapi-sync.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40eba61..84aab50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 permissions: - contents: read + contents: write env: CARGO_TERM_COLOR: always steps: @@ -113,8 +113,22 @@ jobs: if: needs.classify_changes.outputs.run_full_ci != 'true' run: echo "Text-only change detected; skipping workspace test run." - - name: Checkout source - if: needs.classify_changes.outputs.run_full_ci == 'true' + # For same-repo PRs, check out the PR branch head directly so we can push + # a regenerated openapi.json back to it. Fork PRs and push events use the + # default checkout (which is read-only for our purposes). + - name: Checkout (same-repo PR head) + if: | + needs.classify_changes.outputs.run_full_ci == 'true' && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: actions/checkout@v5.0.1 + with: + ref: ${{ github.head_ref }} + + - name: Checkout (default) + if: | + needs.classify_changes.outputs.run_full_ci == 'true' && + !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) uses: actions/checkout@v5.0.1 - name: Install system dependencies @@ -138,8 +152,29 @@ jobs: - name: Run workspace tests if: needs.classify_changes.outputs.run_full_ci == 'true' + # On same-repo PRs, regenerate openapi.json as part of the drift test + # so the following step can commit the update. Elsewhere the env var + # is empty, leaving the drift test in strict-check mode. + env: + OMNIGRAPH_UPDATE_OPENAPI: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) && '1' || '' }} run: cargo test --workspace --locked + - name: Commit regenerated openapi.json + if: | + needs.classify_changes.outputs.run_full_ci == 'true' && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + run: | + if git diff --quiet -- openapi.json; then + echo "openapi.json is already in sync." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add openapi.json + git commit -m "chore: regenerate openapi.json" + git push + test_aws_feature: name: Test omnigraph-server --features aws needs: classify_changes diff --git a/.github/workflows/openapi-sync.yml b/.github/workflows/openapi-sync.yml deleted file mode 100644 index 52bf882..0000000 --- a/.github/workflows/openapi-sync.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Sync OpenAPI - -on: - pull_request: - paths: - - "crates/omnigraph-server/src/**" - - "crates/omnigraph-server/Cargo.toml" - - "crates/omnigraph-server/tests/openapi.rs" - -concurrency: - group: openapi-sync-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - -jobs: - sync: - name: Regenerate openapi.json - # Auto-commit only on same-repo PRs; forks cannot be pushed to. - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout PR branch - uses: actions/checkout@v5.0.1 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y protobuf-compiler libprotobuf-dev - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - - name: Cache Rust build data - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - . -> target - - - name: Regenerate openapi.json - run: | - OMNIGRAPH_UPDATE_OPENAPI=1 \ - cargo test --locked -p omnigraph-server --test openapi openapi_spec_is_up_to_date - - - name: Commit if changed - run: | - if git diff --quiet -- openapi.json; then - echo "openapi.json is already in sync." - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add openapi.json - git commit -m "chore: regenerate openapi.json" - git push diff --git a/crates/omnigraph-server/tests/openapi.rs b/crates/omnigraph-server/tests/openapi.rs index 3c136bf..b257796 100644 --- a/crates/omnigraph-server/tests/openapi.rs +++ b/crates/omnigraph-server/tests/openapi.rs @@ -971,7 +971,10 @@ fn openapi_spec_is_up_to_date() { let generated = serde_json::to_string_pretty(&openapi_doc()).unwrap() + "\n"; - if env::var("OMNIGRAPH_UPDATE_OPENAPI").is_ok() { + if !env::var("OMNIGRAPH_UPDATE_OPENAPI") + .unwrap_or_default() + .is_empty() + { fs::write(&spec_path, &generated).unwrap(); return; } From bcddbdf485b6870692aeebe72570b799f5e3fd4e Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Sun, 19 Apr 2026 12:10:40 +0200 Subject: [PATCH 10/10] Test merge commit; push openapi.json via separate clone Restore the default pull_request checkout (refs/pull/N/merge) so tests see the merged state. The openapi.json auto-commit now uses a separate shallow clone of the PR branch, so the pushed commit contains only the spec change rather than the merge-commit tree. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84aab50..3893efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,22 +113,12 @@ jobs: if: needs.classify_changes.outputs.run_full_ci != 'true' run: echo "Text-only change detected; skipping workspace test run." - # For same-repo PRs, check out the PR branch head directly so we can push - # a regenerated openapi.json back to it. Fork PRs and push events use the - # default checkout (which is read-only for our purposes). - - name: Checkout (same-repo PR head) - if: | - needs.classify_changes.outputs.run_full_ci == 'true' && - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - uses: actions/checkout@v5.0.1 - with: - ref: ${{ github.head_ref }} - - - name: Checkout (default) - if: | - needs.classify_changes.outputs.run_full_ci == 'true' && - !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + # Default checkout: on pull_request this gives us the merge commit + # (refs/pull/N/merge), which is what we want to test. For same-repo PRs + # the regenerated openapi.json is pushed to the head branch below via a + # separate shallow clone. + - name: Checkout source + if: needs.classify_changes.outputs.run_full_ci == 'true' uses: actions/checkout@v5.0.1 - name: Install system dependencies @@ -159,16 +149,32 @@ jobs: OMNIGRAPH_UPDATE_OPENAPI: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) && '1' || '' }} run: cargo test --workspace --locked - - name: Commit regenerated openapi.json + - name: Commit regenerated openapi.json to PR branch if: | needs.classify_changes.outputs.run_full_ci == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + # The workspace was checked out at the PR's merge commit so tests + # see the merged state. Pushing the regenerated openapi.json back + # to the PR branch is done via a separate shallow clone so the + # pushed commit contains only the spec change, not the merge state. if git diff --quiet -- openapi.json; then echo "openapi.json is already in sync." exit 0 fi + tmp=$(mktemp -d) + git clone --depth 1 --branch "${{ github.head_ref }}" \ + "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" \ + "$tmp" + cp openapi.json "$tmp/openapi.json" + cd "$tmp" + if git diff --quiet -- openapi.json; then + echo "openapi.json matches PR branch; nothing to push." + exit 0 + fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add openapi.json