Skip to content

Some updates to the Crossbow report #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
53 changes: 53 additions & 0 deletions .github/workflows/crossbow-nightly-report-r-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Crossbow nightly report R tests

on:
push:
branches: [ main ]
paths:
- 'crossbow-nightly-report/**'
pull_request:
branches: [ main ]
paths:
- 'crossbow-nightly-report/**'

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./crossbow-nightly-report

steps:
- uses: actions/checkout@v3


- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev libssl-dev libxml2-dev

- name: Set up R
uses: r-lib/actions/setup-r@v2
with:
r-version: '4.5.0'
use-public-rspm: true

- name: Restore packages using renv
uses: r-lib/actions/setup-renv@v2
with:
working-directory: ./crossbow-nightly-report

- name: Install test dependencies
run: install.packages('testthat')
shell: Rscript {0}

- name: Run tests
run: |
library(testthat)
test_results <- test_dir("tests", reporter = "summary", stop_on_failure = FALSE)
test_result_df <- as.data.frame(test_results)
# Exit with error code if any tests failed
if (length(test_results) > 0 && any(c(test_result_df$failed, test_result_df$error))) {
quit(status = 1)
}
shell: Rscript {0}
2 changes: 1 addition & 1 deletion .github/workflows/nightly_dashboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:

- uses: r-lib/actions/setup-r@v2
with:
r-version: '4.4.0'
r-version: '4.5.0'
use-public-rspm: true

# Needed due to https://github.com/r-lib/actions/issues/618
Expand Down
140 changes: 99 additions & 41 deletions crossbow-nightly-report/R/functions.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
library(tibble)
library(dplyr)
library(lubridate)
library(glue)
library(tidyr)
Comment on lines +1 to +5
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We get these already in the quarto doc, and this does smell a little funny in a script, but makes testing much easier so I think isn't so bad. Alternatively we could us :: for all of the functions used by these in this cript.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine by me. Doesn't smell too funny to me :)


is_dev <- function() {
Sys.getenv("GITHUB_ACTIONS") != "true"
}

dropdown_helper <- function(values, name, element_id) {
htmltools::tags$select(
# Set to undefined to clear the filter
onchange = glue("Reactable.setFilter('{element_id}', '{name}', event.target.value || undefined)"),
onchange = glue(
"Reactable.setFilter('{element_id}', '{name}', event.target.value || undefined)"
),
# "All" has an empty value to clear the filter, and is the default option
htmltools::tags$option(value = "", "All"),
lapply(unique(values), htmltools::tags$option),
Expand All @@ -14,11 +22,15 @@ dropdown_helper <- function(values, name, element_id) {
}

arrow_commit_links <- function(sha) {
glue("<a href='https://github.com/apache/arrow/commit/{sha}' target='_blank'>{substring(sha, 1, 7)}</a>")
glue(
"<a href='https://github.com/apache/arrow/commit/{sha}' target='_blank'>{substring(sha, 1, 7)}</a>"
)
}

arrow_compare_links <- function(sha1, sha2) {
comp_link <- glue("<a href='https://github.com/apache/arrow/compare/{sha1}...{sha2}' target='_blank'>{substring(sha1, 1, 7)}</a>")
comp_link <- glue(
"<a href='https://github.com/apache/arrow/compare/{sha1}...{sha2}' target='_blank'>{substring(sha1, 1, 7)}</a>"
)

if (rlang::is_empty(comp_link)) {
return(glue("Build has not yet been successful"))
Expand All @@ -34,86 +46,132 @@ make_nice_names <- function(x) {
toTitleCase(gsub("_", " ", names(x)))
}

arrow_build_table <- function(nightly_data, type, task) {
get_commit <- function(df, label) {
df$arrow_commit[df$fail_label == label]
}

arrow_build_table <- function(nightly_data, type, task, to_day = today()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

When I saw this I thought the to_day was actually in the form of to_* thinking that it did something like that. What about something like current_day?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Heh I was being a little cheeky, but yeah readable is more important :P

# Filter data for a specific build type and task
type_task_data <- nightly_data %>%
filter(build_type == type) %>%
filter(task_name == task)

## filter for when the most recent run is a failure
day_window <- today() - 2
# Look at yesterday's date to determine recent failures
# This is used as a window for identifying tasks that failed recently
day_window <- to_day - 1

# Get records where the task failed recently, order by date (newest first)
# and standardize task status values to "pass" and "fail"
ordered_only_recent_fails <- type_task_data %>%
filter(task_name %in% task_name[nightly_date == day_window & task_status != "success"]) %>%
# Only keep records where the task name appears in yesterday's failures
filter(
task_name %in%
task_name[nightly_date == day_window & task_status != "success"]
) %>%
arrange(desc(nightly_date)) %>%
mutate(task_status = case_when(
task_status == "success" ~ "pass",
task_status == "failure" ~ "fail",
TRUE ~ task_status
))
mutate(
task_status = case_when(
task_status == "success" ~ "pass",
task_status == "failure" ~ "fail",
TRUE ~ task_status
)
)

# If there are no recent failures, return a success summary or a null summary if the task is not active
if (nrow(ordered_only_recent_fails) == 0) {
## if there are no failures, return a version of the table that reflects that
# Calculate days since the last run (regardless of status)
days <- as.numeric(
difftime(
ymd(Sys.Date(), tz = "UTC"),
ymd(to_day, tz = "UTC"),
max(type_task_data$nightly_date)
)
)
# Create a summary with success information
success_df <- type_task_data %>%
# Remove stale data by filtering out everything but the last ~2 days of runs
# this makes it so that jobs that have been deleted (but are still in the 120 day look back)
# don't continue to show up.
filter(nightly_date >= to_day - 2) %>%
# Then, take the most recent run since that's all we care about if there are no failures.
slice_max(order_by = nightly_date) %>%
mutate(
since_last_successful_build = days,
last_successful_commit = arrow_commit_links(arrow_commit),
last_successful_build = glue("<a href='{build_links}' target='_blank'>{task_status}</a>"),
last_successful_build = glue(
"<a href='{build_links}' target='_blank'>{task_status}</a>"
),
most_recent_status = "passing"
) %>%
select(task_name, most_recent_status, since_last_successful_build, last_successful_commit, last_successful_build, build_type)
select(
task_name,
most_recent_status,
since_last_successful_build,
last_successful_commit,
last_successful_build,
build_type
)

return(success_df)
}

## find first failure index
# Find the length of the most recent consecutive failure streak
# This uses run length encoding to identify the first sequence of failures
idx_recent_fail <- rle(ordered_only_recent_fails$task_status)$lengths[1]

## expand failure index and give it some names
# Create labels for the failure streak timeline
# This builds a dataframe with positions and labels for the recent failure sequence
failure_df <- tibble(fails_plus_one = seq(1, idx_recent_fail + 1)) %>%
mutate(fail_label = case_when(
fails_plus_one == idx_recent_fail ~ "first_failure",
fails_plus_one == 1 ~ "most_recent_failure",
fails_plus_one == idx_recent_fail + 1 ~ "last_successful_build",
TRUE ~ paste0(fails_plus_one, " days ago")
)) %>%
mutate(
fail_label = case_when(
fails_plus_one == idx_recent_fail ~ "first_failure", # Where the failures began
fails_plus_one == 1 ~ "most_recent_failure", # The most recent failure
fails_plus_one == idx_recent_fail + 1 ~ "last_successful_build", # Last successful build before failures
TRUE ~ paste0(fails_plus_one, " days ago") # General failure timeline
)
) %>%
# Only keep the most recent 9 days of failures or specific labeled events
filter(fails_plus_one <= 9 | grepl("failure|build", fail_label))

## inner_join to ordered data
# Join the failure timeline labels with the actual build data
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for these comments ❤️

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mostly thank Claude :P But in truth I had Claude comment and then went and heavily edited.

df <- ordered_only_recent_fails %>%
rowid_to_column() %>%
inner_join(failure_df, by = c("rowid" = "fails_plus_one"))


# Calculate days since last successful build
if (all(type_task_data$task_status %in% "failure")) {
days <- NA_real_
} else {
## days since last successful build (need to add one)
days <- sum(as.numeric(
difftime(
df$nightly_date[df$fail_label == "most_recent_failure"],
df$nightly_date[df$fail_label == "last_successful_build"]
)
), 1)
}


get_commit <- function(label) {
df$arrow_commit[df$fail_label == label]
# Calculate days between most recent failure and last successful build
# Adding 1 to include the day of the failure
days <- sum(
as.numeric(
difftime(
df$nightly_date[df$fail_label == "most_recent_failure"],
df$nightly_date[df$fail_label == "last_successful_build"]
)
),
1
)
}

# Format the final result as a table with build status information (one row per task)
df %>%
arrange(desc(fail_label)) %>%
mutate(build_links = glue("<a href='{build_links}' target='_blank'>{task_status}</a>")) %>%
mutate(
build_links = glue(
"<a href='{build_links}' target='_blank'>{task_status}</a>"
)
) %>%
select(task_name, build_type, build_links, fail_label) %>%
# Reshape data to have one column for each failure stage
pivot_wider(names_from = fail_label, values_from = build_links) %>%
# Add additional context columns
mutate(
since_last_successful_build = days,
last_successful_commit = arrow_compare_links(get_commit("last_successful_build"), get_commit("first_failure")),
last_successful_commit = arrow_compare_links(
get_commit(df, "last_successful_build"),
get_commit(df, "first_failure")
),
most_recent_status = "failing",
.after = build_type
)
Expand All @@ -124,6 +182,6 @@ crossbow_theme <- function(data, ...) {
tab_options(
table.font.size = 14,
...
) %>%
) %>%
opt_all_caps()
}
2 changes: 2 additions & 0 deletions crossbow-nightly-report/air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[format]
line-width = 120
33 changes: 16 additions & 17 deletions crossbow-nightly-report/crossbow-nightly-report.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ most_recent_commit <- unique(nightly$arrow_commit[nightly$nightly_date == max(ni

<a href='https://arrow.apache.org/'><img src='https://arrow.apache.org/img/arrow-logo_hex_black-txt_white-bg.png' align="right" height="150" /></a>

This report builds in sync with the email notifications (`[email protected]`). `r pluralize('Most recent commit{?s} are: {arrow_commit_links(most_recent_commit)}')`. The report examines data from the last `r lookback_window` days.
This report builds in sync with the email notifications (`[email protected]`). `r pluralize('Most recent commit{?s} {?is/are}: {arrow_commit_links(most_recent_commit)}')`. The report examines data from the last `r lookback_window` days.


# Summary
Expand Down Expand Up @@ -280,7 +280,7 @@ build_table <- map2_df(map_params$build_type, map_params$task_name, ~ arrow_buil
```{r build_table}
bs_nightly_tbl <- build_table %>%
select(where(~ sum(!is.na(.x)) > 0)) %>% ## remove any columns with no data
arrange(build_type, most_recent_status, desc(since_last_successful_build)) %>%
arrange(most_recent_status, build_type, desc(since_last_successful_build)) %>%
rowwise() %>%
mutate(
since_last_successful_build = ifelse(
Expand All @@ -297,12 +297,11 @@ bs_nightly_tbl_sorted <- bs_nightly_tbl[, rev(sort(names(bs_nightly_tbl)))] %>%
relocate(any_of(c("last_successful_build", "first_failure")), .after = last_successful_commit) %>%
relocate(build_type, .before = task_name)

## set a row number such all the failing builds are always displayed
## set a row number such all the failing builds are always displayed with a lower bound of 25
tbl_row_n <- bs_nightly_tbl_sorted %>%
filter(most_recent_status == "failing") %>%
count(build_type) %>%
filter(n == max(n)) %>%
pull(n)
count() %>%
max(., 25)

names(bs_nightly_tbl_sorted) <- make_nice_names(bs_nightly_tbl_sorted)

Expand Down Expand Up @@ -332,13 +331,13 @@ bs_nightly_tbl_sorted %>%
),
columns = list(
`Build Type` = reactable::colDef(
width = 80,
width = 100,
filterInput = function(values, name) {
dropdown_helper(values, name, build_element_id)
}
),
`Task Name` = reactable::colDef(
width = 110
width = 150
),
`Since Last Successful Build` = reactable::colDef(
width = 100,
Expand All @@ -347,22 +346,22 @@ bs_nightly_tbl_sorted %>%
}
),
`Most Recent Status` = reactable::colDef(
width = 70,
width = 85,
filterInput = function(values, name) {
dropdown_helper(values, name, build_element_id)
}
),
`Last Successful Commit` = reactable::colDef(
width = 80
width = 90
),
`Last Successful Build` = reactable::colDef(
width = 80
),
`First Failure` = reactable::colDef(
width = 60
width = 70
),
`Most Recent Failure` = reactable::colDef(
width = 60
width = 70
)
),
elementId = build_element_id
Expand Down Expand Up @@ -408,16 +407,16 @@ nightly_sub_tbl %>%
),
columns = list(
`Task Status` = reactable::colDef(
width = 80,
width = 90,
filterInput = function(values, name) {
dropdown_helper(values, name, error_element_id)
}
),
`Task Name` = reactable::colDef(
width = 200,
filterInput = function(values, name) {
dropdown_helper(values, name, error_element_id)
}
width = 240,
# filterInput = function(values, name) {
# dropdown_helper(values, name, error_element_id)
# }
),
`Build Type` = reactable::colDef(
width = 100,
Expand Down
Loading
Loading