diff --git a/wkfl/src/actions.rs b/wkfl/src/actions.rs index deabdd4..d69dbc4 100644 --- a/wkfl/src/actions.rs +++ b/wkfl/src/actions.rs @@ -946,6 +946,17 @@ fn print_pr_details_markdown(details: &PullRequestDetails) -> anyhow::Result<()> println!("\n## Latest Status\n"); print_json_field("State", status, "state"); print_json_field("Total count", status, "total_count"); + if let Some(contexts) = status + .get("contexts") + .and_then(|contexts| contexts.as_array()) + { + if !contexts.is_empty() { + println!("Contexts:"); + for context in contexts { + print_status_context(context); + } + } + } } if let Some(check_runs) = &details.check_runs { @@ -961,7 +972,23 @@ fn print_pr_details_markdown(details: &PullRequestDetails) -> anyhow::Result<()> let name = json_str(run, "name").unwrap_or("(unnamed)"); let status = json_str(run, "status").unwrap_or("unknown"); let conclusion = json_str(run, "conclusion").unwrap_or("none"); - println!("- {}: {} / {}", name, status, conclusion); + let details_url = json_str(run, "details_url"); + if let Some(details_url) = details_url.filter(|url| !url.is_empty()) { + println!("- {}: {} / {} ({})", name, status, conclusion, details_url); + } else { + println!("- {}: {} / {}", name, status, conclusion); + } + } + } + if let Some(contexts) = check_runs + .get("status_contexts") + .and_then(|contexts| contexts.as_array()) + { + if !contexts.is_empty() { + println!("Status contexts:"); + for context in contexts { + print_status_context(context); + } } } } @@ -1006,7 +1033,16 @@ fn print_pr_details_markdown(details: &PullRequestDetails) -> anyhow::Result<()> .unwrap_or("unknown"); let state = json_str(review, "state").unwrap_or("unknown"); let submitted = json_str(review, "submitted_at").unwrap_or("unknown time"); - println!("- @{}: {} ({})", author, state, submitted); + println!("### Review by @{}\n", author); + println!("- State: {}", state); + println!("- Submitted: {}", submitted); + if let Some(body) = json_str(review, "body") + .map(str::trim) + .filter(|body| !body.is_empty()) + { + println!("\n{}", body); + } + println!(); } } @@ -1049,6 +1085,23 @@ fn print_json_field(label: &str, value: &serde_json::Value, key: &str) { } } +fn print_status_context(context: &serde_json::Value) { + let name = json_str(context, "context").unwrap_or("(unnamed)"); + let state = json_str(context, "state").unwrap_or("unknown"); + let description = + json_str(context, "description").filter(|description| !description.is_empty()); + let target_url = json_str(context, "target_url").filter(|url| !url.is_empty()); + + match (description, target_url) { + (Some(description), Some(target_url)) => { + println!("- {}: {} - {} ({})", name, state, description, target_url); + } + (Some(description), None) => println!("- {}: {} - {}", name, state, description), + (None, Some(target_url)) => println!("- {}: {} ({})", name, state, target_url), + (None, None) => println!("- {}: {}", name, state), + } +} + fn branch_label(branch: Option<&serde_json::Value>) -> String { branch .map(|branch| { diff --git a/wkfl/src/github.rs b/wkfl/src/github.rs index 8498f56..415b20b 100644 --- a/wkfl/src/github.rs +++ b/wkfl/src/github.rs @@ -9,9 +9,9 @@ use crate::gql_queries::pr_details::{ GraphQLPrCommitsPage, GraphQLPrConnectionPageData, GraphQLPrConnectionVariables, GraphQLPrDetailsData, GraphQLPrDetailsVariables, GraphQLPrFilesPage, GraphQLPrReviewRequestsPage, GraphQLPrReviewThreadsPage, GraphQLPrReviewsPage, - GraphQLPullRequest, GraphQLRequestedReviewer, GraphQLReviewConnection, GraphQLReviewNode, - GraphQLReviewRequestConnection, GraphQLReviewThreadCommentsData, - GraphQLReviewThreadCommentsVariables, GraphQLStatusCommit, + GraphQLPrStatusChecksPage, GraphQLPullRequest, GraphQLRequestedReviewer, + GraphQLReviewConnection, GraphQLReviewNode, GraphQLReviewRequestConnection, + GraphQLReviewThreadCommentsData, GraphQLReviewThreadCommentsVariables, GraphQLStatusCommit, }; use crate::gql_queries::prs_to_review::{ GraphQLPrToReviewNode, GraphQLPrsToReviewData, GraphQLPrsToReviewVariables, @@ -369,6 +369,7 @@ impl GitHubClient { comment_cursor: None, thread_cursor: None, review_requests_cursor: None, + status_check_cursor: None, }; let data: GraphQLPrDetailsData = self @@ -406,7 +407,7 @@ impl GitHubClient { let pull_request = pull_request_value(&pr); let mut status = None; let mut check_runs = None; - if let Some(status_commit) = pr + if let Some(mut status_commit) = pr .commits_for_status .nodes .into_iter() @@ -414,6 +415,27 @@ impl GitHubClient { .next() .map(|node| node.commit) { + let mut status_check_page_info = status_commit + .status_check_rollup + .as_ref() + .map(|rollup| rollup.contexts.page_info.clone()); + while let Some(page_info) = status_check_page_info { + if !page_info.has_next_page { + break; + } + let Some(cursor) = page_info.end_cursor.as_deref() else { + break; + }; + let Some(page) = + self.get_pull_request_status_checks_page(owner, repo, pr_number, cursor)? + else { + break; + }; + status_check_page_info = Some(page.page_info.clone()); + if let Some(rollup) = status_commit.status_check_rollup.as_mut() { + rollup.contexts.nodes.extend(page.nodes); + } + } status = status_value(&status_commit); check_runs = Some(check_runs_value(&status_commit)); } @@ -770,6 +792,31 @@ impl GitHubClient { ) } + fn get_pull_request_status_checks_page( + &self, + owner: &str, + repo: &str, + pr_number: u64, + cursor: &str, + ) -> Result> { + let variables = pr_connection_variables(owner, repo, pr_number, cursor); + let data: GraphQLPrConnectionPageData = self + .graphql_query(gql_queries::pr_details::STATUS_CHECKS_QUERY, &variables) + .with_context(|| { + format!("Failed to query GitHub GraphQL API for PR #{pr_number} status checks") + })?; + + let page = pull_request_connection_page(data, owner, repo, pr_number, "status checks")?; + Ok(page + .commits_for_status + .nodes + .into_iter() + .flatten() + .next() + .and_then(|node| node.commit.status_check_rollup) + .map(|rollup| rollup.contexts)) + } + fn get_review_thread_comments_page( &self, thread_id: &str, @@ -1082,6 +1129,7 @@ fn review_value(review: GraphQLReviewNode) -> Value { "user": graphql_author_to_user(review.author), "state": review.state, "submitted_at": review.submitted_at, + "body": review.body, }) } @@ -1095,10 +1143,12 @@ fn issue_comment_from_node(comment: GraphQLIssueCommentNode) -> IssueComment { fn status_value(commit: &GraphQLStatusCommit) -> Option { commit.status.as_ref().map(|status| { + let contexts: Vec = status.contexts.iter().map(status_context_value).collect(); json!({ "sha": commit.oid, "state": status.state.to_lowercase(), "total_count": status.contexts.len(), + "contexts": contexts, }) }) } @@ -1121,10 +1171,21 @@ fn check_runs_value(commit: &GraphQLStatusCommit) -> Value { .filter(|node| node.typename == "CheckRun") .map(check_run_value) .collect(); + let status_contexts: Vec = commit + .status_check_rollup + .as_ref() + .map(|rollup| &rollup.contexts) + .into_iter() + .flat_map(|contexts| contexts.nodes.iter()) + .filter_map(|node| node.as_ref()) + .filter(|node| node.typename == "StatusContext") + .map(status_context_rollup_value) + .collect(); json!({ "total_count": rollup.contexts.total_count, "check_runs": check_runs, + "status_contexts": status_contexts, }) } @@ -1133,6 +1194,25 @@ fn check_run_value(run: &GraphQLCheckRunNode) -> Value { "name": run.name.as_deref().unwrap_or("(unnamed)"), "status": run.status.as_ref().map(|status| status.to_lowercase()).unwrap_or_else(|| "unknown".to_string()), "conclusion": run.conclusion.as_ref().map(|conclusion| conclusion.to_lowercase()), + "details_url": run.details_url, + }) +} + +fn status_context_value(context: &crate::gql_queries::pr_details::GraphQLStatusContext) -> Value { + json!({ + "context": context.context, + "state": context.state.to_lowercase(), + "description": context.description, + "target_url": context.target_url, + }) +} + +fn status_context_rollup_value(context: &GraphQLCheckRunNode) -> Value { + json!({ + "context": context.context.as_deref().unwrap_or("(unnamed)"), + "state": context.state.as_ref().map(|state| state.to_lowercase()).unwrap_or_else(|| "unknown".to_string()), + "description": context.description, + "target_url": context.target_url, }) } diff --git a/wkfl/src/gql_queries/pr_details.graphql b/wkfl/src/gql_queries/pr_details.graphql index 4443f62..8e6bd1d 100644 --- a/wkfl/src/gql_queries/pr_details.graphql +++ b/wkfl/src/gql_queries/pr_details.graphql @@ -7,7 +7,8 @@ query PrDetails( $reviewCursor: String, $commentCursor: String, $threadCursor: String, - $reviewRequestsCursor: String + $reviewRequestsCursor: String, + $statusCheckCursor: String ) { repository(owner: $owner, name: $name) { pullRequest(number: $prNumber) { @@ -86,6 +87,7 @@ query PrDetails( } state submittedAt + body } pageInfo { hasNextPage @@ -143,11 +145,14 @@ query PrDetails( status { state contexts { + context state + description + targetUrl } } statusCheckRollup { - contexts(first: 100) { + contexts(first: 100, after: $statusCheckCursor) { totalCount nodes { __typename @@ -155,8 +160,19 @@ query PrDetails( name status conclusion + detailsUrl + } + ... on StatusContext { + context + state + description + targetUrl } } + pageInfo { + hasNextPage + endCursor + } } } } diff --git a/wkfl/src/gql_queries/pr_details.rs b/wkfl/src/gql_queries/pr_details.rs index b826a4c..7348093 100644 --- a/wkfl/src/gql_queries/pr_details.rs +++ b/wkfl/src/gql_queries/pr_details.rs @@ -10,6 +10,7 @@ pub const REVIEW_THREADS_QUERY: &str = include_str!("pr_details_review_threads.g pub const REVIEW_REQUESTS_QUERY: &str = include_str!("pr_details_review_requests.graphql"); pub const REVIEW_THREAD_COMMENTS_QUERY: &str = include_str!("pr_details_review_thread_comments.graphql"); +pub const STATUS_CHECKS_QUERY: &str = include_str!("pr_details_status_checks.graphql"); #[derive(Debug, Serialize)] pub struct GraphQLPrDetailsVariables<'a> { @@ -29,6 +30,8 @@ pub struct GraphQLPrDetailsVariables<'a> { pub thread_cursor: Option<&'a str>, #[serde(rename = "reviewRequestsCursor")] pub review_requests_cursor: Option<&'a str>, + #[serde(rename = "statusCheckCursor")] + pub status_check_cursor: Option<&'a str>, } #[derive(Debug, Serialize)] @@ -88,6 +91,12 @@ pub struct GraphQLPrReviewRequestsPage { pub review_requests: GraphQLReviewRequestConnection, } +#[derive(Debug, Deserialize)] +pub struct GraphQLPrStatusChecksPage { + #[serde(rename = "commitsForStatus")] + pub commits_for_status: GraphQLStatusCommitConnection, +} + #[derive(Debug, Serialize)] pub struct GraphQLReviewThreadCommentsVariables<'a> { #[serde(rename = "threadId")] @@ -228,6 +237,7 @@ pub struct GraphQLReviewNode { pub state: String, #[serde(rename = "submittedAt")] pub submitted_at: Option, + pub body: String, } #[derive(Debug, Deserialize)] @@ -273,7 +283,16 @@ pub struct GraphQLStatusCommit { #[derive(Debug, Deserialize)] pub struct GraphQLStatus { pub state: String, - pub contexts: Vec, + pub contexts: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct GraphQLStatusContext { + pub context: String, + pub state: String, + pub description: Option, + #[serde(rename = "targetUrl")] + pub target_url: Option, } #[derive(Debug, Deserialize)] @@ -286,6 +305,8 @@ pub struct GraphQLCheckRunConnection { #[serde(rename = "totalCount")] pub total_count: u64, pub nodes: Vec>, + #[serde(rename = "pageInfo")] + pub page_info: GraphQLPageInfo, } #[derive(Debug, Deserialize)] @@ -295,4 +316,11 @@ pub struct GraphQLCheckRunNode { pub name: Option, pub status: Option, pub conclusion: Option, + #[serde(rename = "detailsUrl")] + pub details_url: Option, + pub context: Option, + pub state: Option, + pub description: Option, + #[serde(rename = "targetUrl")] + pub target_url: Option, } diff --git a/wkfl/src/gql_queries/pr_details_reviews.graphql b/wkfl/src/gql_queries/pr_details_reviews.graphql index 5a0c7fa..486b8f1 100644 --- a/wkfl/src/gql_queries/pr_details_reviews.graphql +++ b/wkfl/src/gql_queries/pr_details_reviews.graphql @@ -9,6 +9,7 @@ query PrDetailsReviews($owner: String!, $name: String!, $prNumber: Int!, $cursor } state submittedAt + body } pageInfo { hasNextPage diff --git a/wkfl/src/gql_queries/pr_details_status_checks.graphql b/wkfl/src/gql_queries/pr_details_status_checks.graphql new file mode 100644 index 0000000..3fb772f --- /dev/null +++ b/wkfl/src/gql_queries/pr_details_status_checks.graphql @@ -0,0 +1,37 @@ +query PrDetailsStatusChecks($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) { + repository(owner: $owner, name: $name) { + pullRequest(number: $prNumber) { + commitsForStatus: commits(last: 1) { + nodes { + commit { + oid + statusCheckRollup { + contexts(first: 100, after: $cursor) { + totalCount + nodes { + __typename + ... on CheckRun { + name + status + conclusion + detailsUrl + } + ... on StatusContext { + context + state + description + targetUrl + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } + } + } +}