Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions wkfl/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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!();
}
}

Expand Down Expand Up @@ -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| {
Expand Down
88 changes: 84 additions & 4 deletions wkfl/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -369,6 +369,7 @@ impl GitHubClient {
comment_cursor: None,
thread_cursor: None,
review_requests_cursor: None,
status_check_cursor: None,
};

let data: GraphQLPrDetailsData = self
Expand Down Expand Up @@ -406,14 +407,35 @@ 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()
.flatten()
.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));
}
Expand Down Expand Up @@ -770,6 +792,31 @@ impl GitHubClient {
)
}

fn get_pull_request_status_checks_page(
&self,
owner: &str,
repo: &str,
pr_number: u64,
cursor: &str,
) -> Result<Option<crate::gql_queries::pr_details::GraphQLCheckRunConnection>> {
let variables = pr_connection_variables(owner, repo, pr_number, cursor);
let data: GraphQLPrConnectionPageData<GraphQLPrStatusChecksPage> = 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))
}
Comment on lines +795 to +818

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve the paged commit OID before appending more contexts.

This helper re-runs a query that still resolves commits(last: 1), then throws away the returned oid. If a push lands between page fetches, get_pull_request_details will append contexts from the new head into the old head’s rollup. Return the fetched commit OID with the page and compare it against the initial status_commit.oid before extending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wkfl/src/github.rs` around lines 795 - 818, The status-check paging path in
get_pull_request_status_checks_page drops the fetched commit OID, which can
cause later pages to be merged into the wrong commit’s rollup. Update
GraphQLPrStatusChecksPage and the page handling in get_pull_request_details to
return and compare the commit OID from the paged commits(last: 1) result against
the original status_commit.oid before appending any additional contexts. If the
OIDs differ, stop merging and treat the page as belonging to a different head
commit.


fn get_review_thread_comments_page(
&self,
thread_id: &str,
Expand Down Expand Up @@ -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,
})
}

Expand All @@ -1095,10 +1143,12 @@ fn issue_comment_from_node(comment: GraphQLIssueCommentNode) -> IssueComment {

fn status_value(commit: &GraphQLStatusCommit) -> Option<Value> {
commit.status.as_ref().map(|status| {
let contexts: Vec<Value> = status.contexts.iter().map(status_context_value).collect();
json!({
"sha": commit.oid,
"state": status.state.to_lowercase(),
"total_count": status.contexts.len(),
"contexts": contexts,
})
})
}
Expand All @@ -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<Value> = 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,
})
}

Expand All @@ -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,
})
}

Expand Down
20 changes: 18 additions & 2 deletions wkfl/src/gql_queries/pr_details.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -86,6 +87,7 @@ query PrDetails(
}
state
submittedAt
body
}
pageInfo {
hasNextPage
Expand Down Expand Up @@ -143,20 +145,34 @@ query PrDetails(
status {
state
contexts {
context
state
description
targetUrl
}
}
statusCheckRollup {
contexts(first: 100) {
contexts(first: 100, after: $statusCheckCursor) {
totalCount
nodes {
__typename
... on CheckRun {
name
status
conclusion
detailsUrl
}
... on StatusContext {
context
state
description
targetUrl
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion wkfl/src/gql_queries/pr_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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)]
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -228,6 +237,7 @@ pub struct GraphQLReviewNode {
pub state: String,
#[serde(rename = "submittedAt")]
pub submitted_at: Option<String>,
pub body: String,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -273,7 +283,16 @@ pub struct GraphQLStatusCommit {
#[derive(Debug, Deserialize)]
pub struct GraphQLStatus {
pub state: String,
pub contexts: Vec<serde_json::Value>,
pub contexts: Vec<GraphQLStatusContext>,
}

#[derive(Debug, Deserialize)]
pub struct GraphQLStatusContext {
pub context: String,
pub state: String,
pub description: Option<String>,
#[serde(rename = "targetUrl")]
pub target_url: Option<String>,
}

#[derive(Debug, Deserialize)]
Expand All @@ -286,6 +305,8 @@ pub struct GraphQLCheckRunConnection {
#[serde(rename = "totalCount")]
pub total_count: u64,
pub nodes: Vec<Option<GraphQLCheckRunNode>>,
#[serde(rename = "pageInfo")]
pub page_info: GraphQLPageInfo,
}

#[derive(Debug, Deserialize)]
Expand All @@ -295,4 +316,11 @@ pub struct GraphQLCheckRunNode {
pub name: Option<String>,
pub status: Option<String>,
pub conclusion: Option<String>,
#[serde(rename = "detailsUrl")]
pub details_url: Option<String>,
pub context: Option<String>,
pub state: Option<String>,
pub description: Option<String>,
#[serde(rename = "targetUrl")]
pub target_url: Option<String>,
}
1 change: 1 addition & 0 deletions wkfl/src/gql_queries/pr_details_reviews.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ query PrDetailsReviews($owner: String!, $name: String!, $prNumber: Int!, $cursor
}
state
submittedAt
body
}
pageInfo {
hasNextPage
Expand Down
Loading