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
43 changes: 42 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,18 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
contents: write
env:
CARGO_TERM_COLOR: always
steps:
- name: Skip heavy CI for text-only changes
if: needs.classify_changes.outputs.run_full_ci != 'true'
run: echo "Text-only change detected; skipping workspace test run."

# 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
Expand All @@ -138,8 +142,45 @@ 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 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script injection via github.head_ref in shell command

Medium Severity

${{ github.head_ref }} is interpolated directly into a run: shell script, which is a known GitHub Actions script injection vector. A branch name containing shell metacharacters (e.g., backticks or $()) would execute arbitrary commands in a context where GITHUB_TOKEN with contents: write is available. The safe pattern is to pass the value via an env: mapping (e.g., HEAD_REF: ${{ github.head_ref }}) and reference "$HEAD_REF" in the script instead.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcddbdf. Configure here.

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
git commit -m "chore: regenerate openapi.json"
git push

test_aws_feature:
name: Test omnigraph-server --features aws
needs: classify_changes
Expand Down
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### Cargo features

`omnigraph-server` has an optional `aws` feature that pulls in the AWS
Expand Down
18 changes: 18 additions & 0 deletions crates/omnigraph-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ async fn shutdown_signal() {
get,
path = "/healthz",
tag = "health",
operation_id = "health",
responses(
(status = 200, description = "Server is healthy", body = HealthOutput),
),
Expand Down Expand Up @@ -606,6 +607,7 @@ fn authorize_request(
get,
path = "/snapshot",
tag = "snapshots",
operation_id = "getSnapshot",
params(SnapshotQuery),
responses(
(status = 200, description = "Database snapshot", body = api::SnapshotOutput),
Expand Down Expand Up @@ -646,6 +648,7 @@ async fn server_snapshot(
post,
path = "/read",
tag = "queries",
operation_id = "read",
request_body = ReadRequest,
responses(
(status = 200, description = "Query results", body = ReadOutput),
Expand Down Expand Up @@ -715,6 +718,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"),
Expand Down Expand Up @@ -773,6 +777,7 @@ async fn server_export(
post,
path = "/change",
tag = "mutations",
operation_id = "change",
request_body = ChangeRequest,
responses(
(status = 200, description = "Mutation results", body = ChangeOutput),
Expand Down Expand Up @@ -831,6 +836,7 @@ async fn server_change(
get,
path = "/schema",
tag = "schema",
operation_id = "getSchema",
responses(
(status = 200, description = "Current schema source", body = SchemaOutput),
(status = 401, description = "Unauthorized", body = ErrorOutput),
Expand Down Expand Up @@ -866,6 +872,7 @@ async fn server_schema_get(
post,
path = "/schema/apply",
tag = "mutations",
operation_id = "applySchema",
request_body = SchemaApplyRequest,
responses(
(status = 200, description = "Schema apply results", body = SchemaApplyOutput),
Expand Down Expand Up @@ -904,6 +911,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),
Expand Down Expand Up @@ -973,6 +981,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),
Expand Down Expand Up @@ -1009,6 +1018,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),
Expand Down Expand Up @@ -1056,6 +1066,7 @@ async fn server_branch_create(
delete,
path = "/branches/{branch}",
tag = "branches",
operation_id = "deleteBranch",
params(
("branch" = String, Path, description = "Branch name to delete"),
),
Expand Down Expand Up @@ -1100,6 +1111,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),
Expand Down Expand Up @@ -1145,6 +1157,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),
Expand Down Expand Up @@ -1182,6 +1195,7 @@ async fn server_run_list(
get,
path = "/runs/{run_id}",
tag = "runs",
operation_id = "getRun",
params(
("run_id" = String, Path, description = "Run identifier"),
),
Expand Down Expand Up @@ -1224,6 +1238,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"),
),
Expand Down Expand Up @@ -1273,6 +1288,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"),
),
Expand Down Expand Up @@ -1321,6 +1337,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),
Expand Down Expand Up @@ -1362,6 +1379,7 @@ async fn server_commit_list(
get,
path = "/commits/{commit_id}",
tag = "commits",
operation_id = "getCommit",
params(
("commit_id" = String, Path, description = "Commit identifier"),
),
Expand Down
28 changes: 28 additions & 0 deletions crates/omnigraph-server/tests/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,3 +963,31 @@ 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")
.unwrap_or_default()
.is_empty()
{
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"
);
}
Loading