From ff0208bc491fe97a4022c73c0486e4469213e554 Mon Sep 17 00:00:00 2001 From: Benjamin Rabier Date: Tue, 6 Aug 2024 22:22:31 +0200 Subject: [PATCH 1/3] chore(engine-v2): Add SSE subscription test We were already testing this in the cli tests, so not a huge win but I mostly run integration-tests so well. --- Cargo.lock | 2 + Cargo.toml | 2 + engine/crates/gateway-core/Cargo.toml | 6 +- engine/crates/integration-tests/Cargo.toml | 4 +- .../integration-tests/docker-compose.yml | 17 +- .../integration-tests/src/federation/mod.rs | 192 +----------------- .../src/federation/request.rs | 160 +++++++++++++++ .../src/federation/request/stream.rs | 91 +++++++++ .../tests/federation/subscriptions/mod.rs | 2 + .../multipart.rs} | 0 .../tests/federation/subscriptions/sse.rs | 118 +++++++++++ 11 files changed, 396 insertions(+), 198 deletions(-) create mode 100644 engine/crates/integration-tests/src/federation/request.rs create mode 100644 engine/crates/integration-tests/src/federation/request/stream.rs create mode 100644 engine/crates/integration-tests/tests/federation/subscriptions/mod.rs rename engine/crates/integration-tests/tests/federation/{subscriptions.rs => subscriptions/multipart.rs} (100%) create mode 100644 engine/crates/integration-tests/tests/federation/subscriptions/sse.rs diff --git a/Cargo.lock b/Cargo.lock index e35696413..df7ed0bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4604,9 +4604,11 @@ dependencies = [ "async-graphql-parser", "async-once-cell", "async-runtime", + "async-sse", "async-trait", "axum", "base64 0.22.1", + "bytes", "common-types", "const_format", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 67c40ddd2..26dff2a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ async-graphql = "7.0.3" async-graphql-axum = "7.0.3" async-graphql-parser = "7.0.3" async-graphql-value = "7.0.3" +async-sse = "5" async-trait = "0.1.80" axum = { version = "0.7.5", default-features = false } axum-server = { version = "0.6", default-features = false } @@ -102,6 +103,7 @@ internment = { version = "0.8", features = ["serde", "arc"] } itertools = "0.13.0" jsonwebtoken = "9.3.0" governor = "0.6" +multipart-stream = "0.1.2" num-traits = "0.2.18" once_cell = "1.19.0" openidconnect = "4.0.0-alpha.1" diff --git a/engine/crates/gateway-core/Cargo.toml b/engine/crates/gateway-core/Cargo.toml index f6a3cf16c..86eed627b 100644 --- a/engine/crates/gateway-core/Cargo.toml +++ b/engine/crates/gateway-core/Cargo.toml @@ -19,8 +19,8 @@ workspace = true [dependencies] async-graphql.workspace = true async-runtime.workspace = true -async-sse = "5.1.0" -async-trait = "0.1.80" +async-sse.workspace = true +async-trait.workspace = true blake3.workspace = true bytes.workspace = true common-types.workspace = true @@ -36,7 +36,7 @@ headers.workspace = true http.workspace = true mediatype = "0.19.18" mime = "0.3.17" -multipart-stream = "0.1.2" +multipart-stream.workspace = true operation-normalizer = { path = "../operation-normalizer" } partial-caching.workspace = true registry-for-cache.workspace = true diff --git a/engine/crates/integration-tests/Cargo.toml b/engine/crates/integration-tests/Cargo.toml index bac00dc5c..58f916d27 100644 --- a/engine/crates/integration-tests/Cargo.toml +++ b/engine/crates/integration-tests/Cargo.toml @@ -12,7 +12,9 @@ async-graphql-parser.workspace = true async-graphql.workspace = true async-once-cell = "0.5.3" async-runtime.workspace = true +async-sse.workspace = true async-trait.workspace = true +bytes.workspace = true crossbeam-queue = "0.3" cynic.workspace = true cynic-introspection.workspace = true @@ -32,7 +34,7 @@ headers.workspace = true http.workspace = true indoc = "2.0.5" insta.workspace = true -multipart-stream = "0.1.2" +multipart-stream.workspace = true names = "0.14.1-dev" openidconnect.workspace = true reqwest.workspace = true diff --git a/engine/crates/integration-tests/docker-compose.yml b/engine/crates/integration-tests/docker-compose.yml index c88438bc6..dfd35cdb0 100644 --- a/engine/crates/integration-tests/docker-compose.yml +++ b/engine/crates/integration-tests/docker-compose.yml @@ -1,9 +1,16 @@ version: '3' services: + sse-subgraph: + restart: unless-stopped + build: + context: ./data/sse-subgraph + ports: + - '4092:4092' + # MongoDB data-api: image: grafbase/mongodb-data-api:latest - restart: always + restart: unless-stopped environment: MONGODB_DATABASE_URL: 'mongodb://grafbase:grafbase@mongodb:27017' ports: @@ -15,7 +22,7 @@ services: mongodb: image: mongo:latest - restart: always + restart: unless-stopped environment: MONGO_INITDB_ROOT_USERNAME: 'grafbase' MONGO_INITDB_ROOT_PASSWORD: 'grafbase' @@ -28,7 +35,7 @@ services: # Postgres postgres: image: postgres:16 - restart: always + restart: unless-stopped command: postgres -c 'max_connections=1000' environment: POSTGRES_PASSWORD: 'grafbase' @@ -61,7 +68,7 @@ services: environment: DSN: 'sqlite:///var/lib/sqlite/db.sqlite?_fk=true' URLS_SELF_ISSUER: 'http://127.0.0.1:4444' - restart: always + restart: unless-stopped depends_on: - hydra-migrate networks: @@ -104,7 +111,7 @@ services: URLS_SELF_ISSUER: 'http://127.0.0.1:4454' SERVE_PUBLIC_PORT: '4454' SERVE_ADMIN_PORT: '4455' - restart: always + restart: unless-stopped depends_on: - hydra-migrate networks: diff --git a/engine/crates/integration-tests/src/federation/mod.rs b/engine/crates/integration-tests/src/federation/mod.rs index 7970036a7..18adbcbc1 100644 --- a/engine/crates/integration-tests/src/federation/mod.rs +++ b/engine/crates/integration-tests/src/federation/mod.rs @@ -1,24 +1,11 @@ mod builder; +mod request; -use std::{ - any::TypeId, - borrow::Cow, - collections::HashMap, - future::IntoFuture, - ops::{Deref, DerefMut}, - str::FromStr, - sync::Arc, -}; +use std::{any::TypeId, collections::HashMap, sync::Arc}; pub use builder::*; -use engine::{BatchRequest, Variables}; -use engine_v2::{HttpGraphqlResponse, HttpGraphqlResponseBody}; -use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt}; -use gateway_core::StreamingFormat; use graphql_mocks::{MockGraphQlServer, ReceivedRequest}; -use headers::HeaderMapExt; -use http::{header::Entry, HeaderName, HeaderValue}; -use serde::de::Error; +pub use request::*; use crate::engine_v1::GraphQlRequest; @@ -69,176 +56,3 @@ impl TestEngineV2 { .collect() } } - -#[must_use] -pub struct ExecutionRequest { - request: GraphQlRequest, - #[allow(dead_code)] - headers: Vec<(String, String)>, - engine: Arc>, -} - -impl ExecutionRequest { - pub fn by_client(self, name: &'static str, version: &'static str) -> Self { - self.header("x-grafbase-client-name", name) - .header("x-grafbase-client-version", version) - } - - /// Adds a header into the request - pub fn header(mut self, name: impl Into, value: impl Into) -> Self { - self.headers.push((name.into(), value.into())); - self - } - - pub fn variables(mut self, variables: impl serde::Serialize) -> Self { - self.request.variables = Some(Variables::from_json( - serde_json::to_value(variables).expect("variables to be serializable"), - )); - self - } - - pub fn extensions(mut self, extensions: impl serde::Serialize) -> Self { - self.request.extensions = - serde_json::from_value(serde_json::to_value(extensions).expect("extensions to be serializable")) - .expect("extensions to be deserializable"); - self - } - - fn http_headers(&self) -> http::HeaderMap { - let mut headers = http::HeaderMap::new(); - - for (key, value) in &self.headers { - let key = HeaderName::from_str(key).unwrap(); - let value = HeaderValue::from_str(value).unwrap(); - - if let Entry::Occupied(mut e) = headers.entry(key.clone()) { - e.append(value); - } else { - headers.insert(key, value); - } - } - - headers - } - - pub fn into_multipart_stream(self) -> MultipartStreamRequest { - MultipartStreamRequest(self) - } -} - -impl IntoFuture for ExecutionRequest { - type Output = GraphqlResponse; - - type IntoFuture = BoxFuture<'static, Self::Output>; - - fn into_future(self) -> Self::IntoFuture { - let headers = self.http_headers(); - let request = BatchRequest::Single(self.request.into_engine_request()); - Box::pin(async move { self.engine.execute(headers, request).await.try_into().unwrap() }) - } -} - -pub struct MultipartStreamRequest(ExecutionRequest); - -impl MultipartStreamRequest { - pub async fn collect(self) -> B - where - B: Default + Extend, - { - self.await.stream.collect().await - } -} - -impl IntoFuture for MultipartStreamRequest { - type Output = GraphqlStreamingResponse; - - type IntoFuture = BoxFuture<'static, Self::Output>; - - fn into_future(self) -> Self::IntoFuture { - let mut headers = self.0.http_headers(); - headers.typed_insert(StreamingFormat::IncrementalDelivery); - let request = BatchRequest::Single(self.0.request.into_engine_request()); - Box::pin(async move { - let response = self.0.engine.execute(headers, request).await; - let stream = multipart_stream::parse(response.body.into_stream().map_ok(Into::into), "-") - .map(|result| serde_json::from_slice(&result.unwrap().body).unwrap()); - GraphqlStreamingResponse { - stream: Box::pin(stream), - headers: response.headers, - } - }) - } -} - -pub struct GraphqlStreamingResponse { - pub stream: BoxStream<'static, serde_json::Value>, - pub headers: http::HeaderMap, -} - -#[derive(serde::Serialize, Debug)] -pub struct GraphqlResponse { - #[serde(flatten)] - pub body: serde_json::Value, - #[serde(skip)] - pub headers: http::HeaderMap, -} - -impl TryFrom for GraphqlResponse { - type Error = serde_json::Error; - - fn try_from(response: HttpGraphqlResponse) -> Result { - Ok(GraphqlResponse { - body: match response.body { - HttpGraphqlResponseBody::Bytes(bytes) => serde_json::from_slice(bytes.as_ref())?, - HttpGraphqlResponseBody::Stream(_) => { - return Err(serde_json::Error::custom("Unexpected stream response body"))? - } - }, - headers: response.headers, - }) - } -} - -impl std::fmt::Display for GraphqlResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", serde_json::to_string_pretty(&self.body).unwrap()) - } -} - -impl Deref for GraphqlResponse { - type Target = serde_json::Value; - - fn deref(&self) -> &Self::Target { - &self.body - } -} - -impl DerefMut for GraphqlResponse { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.body - } -} - -impl GraphqlResponse { - pub fn into_value(self) -> serde_json::Value { - self.body - } - - #[track_caller] - pub fn into_data(self) -> serde_json::Value { - assert!(self.errors().is_empty(), "{self:#?}"); - - match self.body { - serde_json::Value::Object(mut value) => value.remove("data"), - _ => None, - } - .unwrap_or_default() - } - - pub fn errors(&self) -> Cow<'_, Vec> { - self.body["errors"] - .as_array() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(Vec::new())) - } -} diff --git a/engine/crates/integration-tests/src/federation/request.rs b/engine/crates/integration-tests/src/federation/request.rs new file mode 100644 index 000000000..cf0292185 --- /dev/null +++ b/engine/crates/integration-tests/src/federation/request.rs @@ -0,0 +1,160 @@ +mod stream; + +use std::{ + borrow::Cow, + future::IntoFuture, + ops::{Deref, DerefMut}, + str::FromStr, + sync::Arc, +}; + +use engine::{BatchRequest, Variables}; +use engine_v2::{HttpGraphqlResponse, HttpGraphqlResponseBody}; +use futures::future::BoxFuture; +use http::{header::Entry, HeaderName, HeaderValue}; +use serde::de::Error; +pub use stream::*; + +use crate::engine_v1::GraphQlRequest; + +use super::TestRuntime; + +#[must_use] +pub struct ExecutionRequest { + pub(super) request: GraphQlRequest, + #[allow(dead_code)] + pub(super) headers: Vec<(String, String)>, + pub(super) engine: Arc>, +} + +impl ExecutionRequest { + pub fn by_client(self, name: &'static str, version: &'static str) -> Self { + self.header("x-grafbase-client-name", name) + .header("x-grafbase-client-version", version) + } + + /// Adds a header into the request + pub fn header(mut self, name: impl Into, value: impl Into) -> Self { + self.headers.push((name.into(), value.into())); + self + } + + pub fn variables(mut self, variables: impl serde::Serialize) -> Self { + self.request.variables = Some(Variables::from_json( + serde_json::to_value(variables).expect("variables to be serializable"), + )); + self + } + + pub fn extensions(mut self, extensions: impl serde::Serialize) -> Self { + self.request.extensions = + serde_json::from_value(serde_json::to_value(extensions).expect("extensions to be serializable")) + .expect("extensions to be deserializable"); + self + } + + fn http_headers(&self) -> http::HeaderMap { + let mut headers = http::HeaderMap::new(); + + for (key, value) in &self.headers { + let key = HeaderName::from_str(key).unwrap(); + let value = HeaderValue::from_str(value).unwrap(); + + if let Entry::Occupied(mut e) = headers.entry(key.clone()) { + e.append(value); + } else { + headers.insert(key, value); + } + } + + headers + } + + pub fn into_multipart_stream(self) -> MultipartStreamRequest { + MultipartStreamRequest(self) + } + + pub fn into_see_stream(self) -> SseStreamRequest { + SseStreamRequest(self) + } +} + +impl IntoFuture for ExecutionRequest { + type Output = GraphqlResponse; + + type IntoFuture = BoxFuture<'static, Self::Output>; + + fn into_future(self) -> Self::IntoFuture { + let headers = self.http_headers(); + let request = BatchRequest::Single(self.request.into_engine_request()); + Box::pin(async move { self.engine.execute(headers, request).await.try_into().unwrap() }) + } +} + +#[derive(serde::Serialize, Debug)] +pub struct GraphqlResponse { + #[serde(flatten)] + pub body: serde_json::Value, + #[serde(skip)] + pub headers: http::HeaderMap, +} + +impl TryFrom for GraphqlResponse { + type Error = serde_json::Error; + + fn try_from(response: HttpGraphqlResponse) -> Result { + Ok(GraphqlResponse { + body: match response.body { + HttpGraphqlResponseBody::Bytes(bytes) => serde_json::from_slice(bytes.as_ref())?, + HttpGraphqlResponseBody::Stream(_) => { + return Err(serde_json::Error::custom("Unexpected stream response body"))? + } + }, + headers: response.headers, + }) + } +} + +impl std::fmt::Display for GraphqlResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string_pretty(&self.body).unwrap()) + } +} + +impl Deref for GraphqlResponse { + type Target = serde_json::Value; + + fn deref(&self) -> &Self::Target { + &self.body + } +} + +impl DerefMut for GraphqlResponse { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.body + } +} + +impl GraphqlResponse { + pub fn into_value(self) -> serde_json::Value { + self.body + } + + #[track_caller] + pub fn into_data(self) -> serde_json::Value { + assert!(self.errors().is_empty(), "{self:#?}"); + + match self.body { + serde_json::Value::Object(mut value) => value.remove("data"), + _ => None, + } + .unwrap_or_default() + } + + pub fn errors(&self) -> Cow<'_, Vec> { + self.body["errors"] + .as_array() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(Vec::new())) + } +} diff --git a/engine/crates/integration-tests/src/federation/request/stream.rs b/engine/crates/integration-tests/src/federation/request/stream.rs new file mode 100644 index 000000000..f98a24e78 --- /dev/null +++ b/engine/crates/integration-tests/src/federation/request/stream.rs @@ -0,0 +1,91 @@ +use std::future::IntoFuture; + +use bytes::Bytes; +use engine::BatchRequest; +use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt}; +use gateway_core::StreamingFormat; +use headers::HeaderMapExt; + +pub struct MultipartStreamRequest(pub(super) super::ExecutionRequest); + +impl MultipartStreamRequest { + pub async fn collect(self) -> B + where + B: Default + Extend, + { + self.await.stream.collect().await + } +} + +impl IntoFuture for MultipartStreamRequest { + type Output = GraphqlStreamingResponse; + + type IntoFuture = BoxFuture<'static, Self::Output>; + + fn into_future(self) -> Self::IntoFuture { + let mut headers = self.0.http_headers(); + headers.typed_insert(StreamingFormat::IncrementalDelivery); + let request = BatchRequest::Single(self.0.request.into_engine_request()); + Box::pin(async move { + let response = self.0.engine.execute(headers, request).await; + let stream = multipart_stream::parse(response.body.into_stream().map_ok(Into::into), "-") + .map(|result| serde_json::from_slice(&result.unwrap().body).unwrap()); + GraphqlStreamingResponse { + headers: response.headers, + stream: Box::pin(stream), + } + }) + } +} + +pub struct SseStreamRequest(pub(super) super::ExecutionRequest); + +impl SseStreamRequest { + pub async fn collect(self) -> B + where + B: Default + Extend, + { + self.await.stream.collect().await + } +} + +impl IntoFuture for SseStreamRequest { + type Output = GraphqlStreamingResponse; + type IntoFuture = BoxFuture<'static, Self::Output>; + fn into_future(self) -> Self::IntoFuture { + let mut headers = self.0.http_headers(); + headers.typed_insert(StreamingFormat::GraphQLOverSSE); + let request = BatchRequest::Single(self.0.request.into_engine_request()); + Box::pin(async move { + let response = self.0.engine.execute(headers, request).await; + let stream = response.body.into_stream().map(|result| match result { + Ok(bytes) => Ok(Bytes::from(bytes)), + Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)), + }); + let stream = async_sse::decode(stream.into_async_read()) + .into_stream() + .try_take_while(|event| { + let take = if let async_sse::Event::Message(msg) = event { + msg.name() != "complete" + } else { + false + }; + futures::future::ready(Ok(take)) + }) + .map(|result| match result { + Ok(async_sse::Event::Retry(_)) => serde_json::Value::String("Got retry?".into()), + Ok(async_sse::Event::Message(msg)) => serde_json::from_slice(msg.data()).unwrap(), + Err(err) => serde_json::Value::String(err.to_string()), + }); + GraphqlStreamingResponse { + headers: response.headers, + stream: Box::pin(stream), + } + }) + } +} + +pub struct GraphqlStreamingResponse { + pub headers: http::HeaderMap, + pub stream: BoxStream<'static, serde_json::Value>, +} diff --git a/engine/crates/integration-tests/tests/federation/subscriptions/mod.rs b/engine/crates/integration-tests/tests/federation/subscriptions/mod.rs new file mode 100644 index 000000000..b8d852545 --- /dev/null +++ b/engine/crates/integration-tests/tests/federation/subscriptions/mod.rs @@ -0,0 +1,2 @@ +mod multipart; +mod sse; diff --git a/engine/crates/integration-tests/tests/federation/subscriptions.rs b/engine/crates/integration-tests/tests/federation/subscriptions/multipart.rs similarity index 100% rename from engine/crates/integration-tests/tests/federation/subscriptions.rs rename to engine/crates/integration-tests/tests/federation/subscriptions/multipart.rs diff --git a/engine/crates/integration-tests/tests/federation/subscriptions/sse.rs b/engine/crates/integration-tests/tests/federation/subscriptions/sse.rs new file mode 100644 index 000000000..6655faedc --- /dev/null +++ b/engine/crates/integration-tests/tests/federation/subscriptions/sse.rs @@ -0,0 +1,118 @@ +use engine_v2::Engine; +use graphql_mocks::{ + FederatedAccountsSchema, FederatedInventorySchema, FederatedProductsSchema, FederatedReviewsSchema, +}; +use integration_tests::{federation::EngineV2Ext, runtime}; + +#[test] +fn single_subgraph_subscription() { + let response = runtime().block_on(async move { + let engine = Engine::builder() + .with_subgraph(FederatedProductsSchema) + .with_sdl_websocket_config() + .build() + .await; + + engine + .execute( + r" + subscription { + newProducts { + upc + name + price + } + } + ", + ) + .into_see_stream() + .collect::>() + .await + }); + + insta::assert_json_snapshot!(response, @r###" + [ + { + "data": { + "newProducts": { + "upc": "top-4", + "name": "Jeans", + "price": 44 + } + } + }, + { + "data": { + "newProducts": { + "upc": "top-5", + "name": "Pink Jeans", + "price": 55 + } + } + } + ] + "###); +} + +#[test] +fn actual_federated_subscription() { + let response = runtime().block_on(async move { + let engine = Engine::builder() + .with_subgraph(FederatedAccountsSchema) + .with_subgraph(FederatedProductsSchema) + .with_subgraph(FederatedReviewsSchema) + .with_subgraph(FederatedInventorySchema) + .with_sdl_websocket_config() + .build() + .await; + + engine + .execute( + r" + subscription { + newProducts { + upc + name + reviews { + author { + username + } + body + } + } + } + ", + ) + .into_see_stream() + .collect::>() + .await + }); + + insta::assert_json_snapshot!(response, @r###" + [ + { + "data": { + "newProducts": { + "upc": "top-4", + "name": "Jeans", + "reviews": [] + } + } + }, + { + "data": { + "newProducts": { + "upc": "top-5", + "name": "Pink Jeans", + "reviews": [ + { + "author": null, + "body": "Beautiful Pink, my parrot loves it. Definitely recommend!" + } + ] + } + } + } + ] + "###); +} From da48890c713b8188f8078b0e17d6ddffa813ef58 Mon Sep 17 00:00:00 2001 From: hackal Date: Wed, 7 Aug 2024 11:12:23 +0200 Subject: [PATCH 2/3] add callstack config files --- .callstack.yml | 39 ++++++++++++++++++++++++ .github/workflows/callstack-reviewer.yml | 28 +++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 .callstack.yml create mode 100644 .github/workflows/callstack-reviewer.yml diff --git a/.callstack.yml b/.callstack.yml new file mode 100644 index 000000000..5e25f11d6 --- /dev/null +++ b/.callstack.yml @@ -0,0 +1,39 @@ +pr_review: + # Default: true + auto_run: true + modules: + # Automatically create a description summarizing the changes in pull request. + description: + enabled: true + diagram: false + + # Find potential bugs in pull request changes or related files. + bug_hunter: + enabled: true + # Include fixes to possible bugs. + suggestions: true + + # Suggest improvements to added code. + code_suggestions: + enabled: true + + # Suggest changes to follow defined code conventions. + code_conventions: + enabled: false + # Describe your code conventions in plain text. + conventions: | + E.g. Exported variables, functions, classes and methods should be defined before private. + + + # Point out any typos or grammatical errors in variable names, texts, comments. + grammar: + enabled: false + + # Suggest performance improvements to added code. + performance: + enabled: true + + # Find potential security issues in added code. + security: + enabled: true + diff --git a/.github/workflows/callstack-reviewer.yml b/.github/workflows/callstack-reviewer.yml new file mode 100644 index 000000000..3452b7c3f --- /dev/null +++ b/.github/workflows/callstack-reviewer.yml @@ -0,0 +1,28 @@ +name: Callstack.ai PR Review + +on: + workflow_dispatch: + inputs: + config: + type: string + description: "config for reviewer" + required: true + head: + type: string + description: "head commit sha" + required: true + base: + type: string + description: "base commit sha" + required: false + +jobs: + callstack_pr_review_job: + runs-on: ubuntu-latest + steps: + - name: Review PR + uses: callstackai/action@main + with: + config: ${{ inputs.config }} + head: ${{ inputs.head }} + From bb4b634a9e608666600f5549fed1358c916a62d7 Mon Sep 17 00:00:00 2001 From: Adam Pavlisin Date: Fri, 9 Aug 2024 10:16:15 +0200 Subject: [PATCH 3/3] Update callstack-reviewer.yml --- .github/workflows/callstack-reviewer.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/callstack-reviewer.yml b/.github/workflows/callstack-reviewer.yml index 3452b7c3f..712bb6891 100644 --- a/.github/workflows/callstack-reviewer.yml +++ b/.github/workflows/callstack-reviewer.yml @@ -25,4 +25,5 @@ jobs: with: config: ${{ inputs.config }} head: ${{ inputs.head }} + export: /code/chats.json