From a71048b68cbe2125175d5ce2a86bdd6a306602f2 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 15:31:43 +0100 Subject: [PATCH 01/17] Add checks for prod repos --- .lintr | 8 ++ DESCRIPTION | 1 + R/codeowners.R | 62 +++++++++++++++ R/dependencies.R | 61 +++++++++++++++ R/generate_package_table.R | 4 +- R/get_repos.R | 20 +++++ R/prod_checks.R | 26 +++++++ R/repo_lists.R | 23 ++++++ github-repos/github-repos.Rmd | 80 ++++++++++++++++++++ tests/testthat/test-generate_package_table.R | 2 +- 10 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 .lintr create mode 100644 R/codeowners.R create mode 100644 R/dependencies.R create mode 100644 R/get_repos.R create mode 100644 R/prod_checks.R create mode 100644 R/repo_lists.R create mode 100644 github-repos/github-repos.Rmd diff --git a/.lintr b/.lintr new file mode 100644 index 0000000..9ae5123 --- /dev/null +++ b/.lintr @@ -0,0 +1,8 @@ +linters: all_linters() +exclusions: list( + "tests/testthat.R", + "R/generate_package_table.R", + "R/repo_lists.R" = c( + nonportable_path_linter = Inf + ) + ) diff --git a/DESCRIPTION b/DESCRIPTION index 3bad932..62700e5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,6 +10,7 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.1 Suggests: + memoise, testthat (>= 3.0.0) Config/testthat/edition: 3 Imports: diff --git a/R/codeowners.R b/R/codeowners.R new file mode 100644 index 0000000..587b219 --- /dev/null +++ b/R/codeowners.R @@ -0,0 +1,62 @@ +get_codeowner <- function( + repo_fullname, + path = ".github/CODEOWNERS" # nolint: non_portable_path +) { + raw_content <- tryCatch( + expr = { + raw_content <- gh::gh( + "GET /repos/{repo}/contents/{path}", # nolint: non_portable_path + repo = repo_fullname, + path = path + ) + }, + error = function(e) { + NA_character_ + } + ) + if (identical(raw_content, NA_character_)) { + # Early return + return(NA_character_) + } + content_b64 <- raw_content[["content"]] + content_char <- rawToChar(base64enc::base64decode(content_b64)) + content_words <- strsplit(content_char, "\n", fixed = TRUE)[[1L]] + default_owner <- grep( + pattern = "^\\s*\\*\\s+@\\S+", + x = content_words, + value = TRUE + ) + default_owner_clean <- gsub( + pattern = "^\\s*\\*\\s+@|\\s*$", + replacement = "", + x = default_owner + ) + return(default_owner_clean) +} +if (requireNamespace("memoise")) { + get_codeowner <- memoise::memoise(get_codeowner) +} + + +get_codeowner_errors <- function(repo_fullname) { + raw_content <- tryCatch( + expr = { + raw_content <- gh::gh( + "GET /repos/{repo}/codeowners/errors", # nolint: non_portable_path + repo = repo_fullname + ) + }, + error = function(e) { + NA_character_ + } + ) + if (identical(raw_content, NA_character_)) { + # Early return + return(NA_integer_) + } + errors <- raw_content[["errors"]] + return(length(errors)) +} +if (requireNamespace("memoise")) { + get_codeowner_errors <- memoise::memoise(get_codeowner_errors) +} diff --git a/R/dependencies.R b/R/dependencies.R new file mode 100644 index 0000000..798c094 --- /dev/null +++ b/R/dependencies.R @@ -0,0 +1,61 @@ +identify_gh_deps <- function(repo_fullname) { + deps <- tryCatch( + expr = {pak::pkg_deps(repo_fullname)}, + error = function(e) { + NULL + } + ) + if (is.null(deps)) { + return(NA_character_) + } + gh_deps <- deps[["type"]] == "github" + gh_dep_refs <- deps[gh_deps, "ref"] + return(gh_dep_refs) +} +if (requireNamespace("memoise")) { + identify_gh_deps <- memoise::memoise(identify_gh_deps) +} + +identify_if_dep <- function( + repo_fullname, + repos_to_check = list( + prod = c(prod_workflows, r2dii.repos), + experimental = experimental_workflows + ), + return_max = TRUE +) { + + dep_lists <- lapply(repos_to_check, function(repos) { + deps <- lapply( + X = repos, + FUN = identify_gh_deps + ) + clean_deps <- unique(unlist(deps)) + return(clean_deps) + }) + + dep_levels <- NULL + for (i in seq_along(dep_lists)) { + if ( + repo_fullname %in% dep_lists[[i]] || + repo_fullname %in% repos_to_check[[i]] + ) { + dep_levels <- c(names(dep_lists)[i], dep_levels) + } + } + if (is.null(dep_levels)) { + dep_levels <- NA_character_ + } + + dep_levels_factor <- factor( + x = dep_levels, + levels = c("experimental", "prod"), + ordered = TRUE + ) + + if (return_max) { + return(max(dep_levels_factor)) + } else { + return(dep_levels_factor) + } +} diff --git a/R/generate_package_table.R b/R/generate_package_table.R index 8568f7e..7a968d5 100644 --- a/R/generate_package_table.R +++ b/R/generate_package_table.R @@ -68,9 +68,9 @@ format_maintainer <- function(maintainer) { table_lifecycle <- function(repo_path) { readme <- fetch_readme(repo_path) - pattern <- "https://img.shields.io/badge/lifecycle-.*.svg" + pattern <- "https://img.shields.io/badge/lifecycle-\\S+.svg" - lifecycle_badge <- readme[grepl(pattern, readme)] + lifecycle_badge <- readme[grepl(pattern, readme)][1] lifecycle_badge <- gsub(".*(https[^)]*\\.svg).*", "\\1", lifecycle_badge) return(lifecycle_badge) diff --git a/R/get_repos.R b/R/get_repos.R new file mode 100644 index 0000000..797023a --- /dev/null +++ b/R/get_repos.R @@ -0,0 +1,20 @@ +get_repos <- function(org) { + repo_fetch <- gh::gh( + "GET /orgs/{org}/repos", # nolint: non_portable_path + org = org, per_page = 20L) + all_repos <- repo_fetch + while ( + grepl( + pattern = "rel=\"next\"", + x = attr(repo_fetch, "response")[["link"]], + fixed = TRUE + ) + ) { + repo_fetch <- gh::gh_next(repo_fetch) + all_repos <- c(all_repos, repo_fetch) + } + return(all_repos) +} +if (requireNamespace("memoise")) { + get_repos <- memoise::memoise(get_repos) +} diff --git a/R/prod_checks.R b/R/prod_checks.R new file mode 100644 index 0000000..56233b0 --- /dev/null +++ b/R/prod_checks.R @@ -0,0 +1,26 @@ +prod_checks <- function(repo_json) { + if (is.null(repo_json[["codeowner"]])) { + codeowner <- get_codeowner(repo_json[["full_name"]]) + repo_json[["codeowner"]] <- codeowner + } + if (is.null(repo_json[["codeowner_errors"]])) { + codeowner_errors <- get_codeowner_errors(repo_json[["full_name"]]) + repo_json[["codeowner_errors"]] <- codeowner_errors + } + if (is.null(repo_json[["gh_deps"]])) { + gh_deps <- identify_gh_deps(repo_json[["full_name"]]) + repo_json[["gh_deps"]] <- gh_deps + } + if (is.null(repo_json[["dep_tree"]])) { + dep_tree <- identify_if_dep(repo_json[["full_name"]]) + repo_json[["dep_tree"]] <- dep_tree + } + if (is.null(repo_json[["lifecycle_badge"]])) { + lifecycle_badge <- format_lifecycle(table_lifecycle(repo_json[["full_name"]])) + repo_json[["lifecycle_badge"]] <- lifecycle_badge + } + return(repo_json) +} +if (requireNamespace("memoise")) { + prod_checks <- memoise::memoise(prod_checks) +} diff --git a/R/repo_lists.R b/R/repo_lists.R new file mode 100644 index 0000000..d872fb7 --- /dev/null +++ b/R/repo_lists.R @@ -0,0 +1,23 @@ +prod_workflows <- c( + "RMI-PACTA/pactaverse", + "RMI-PACTA/workflow.data.preparation", + "RMI-PACTA/workflow.mfm2023", + "RMI-PACTA/workflow.portfolio.parsing", + "RMI-PACTA/workflow.prepare.pacta.indices", + "RMI-PACTA/workflow.scenario.preparation", + "RMI-PACTA/workflow.transition.monitor" +) + +r2dii.repos <- c( + "RMI-PACTA/r2dii.data", + "RMI-PACTA/r2dii.match", + "RMI-PACTA/r2dii.analysis", + "RMI-PACTA/r2dii.plot" +) + +experimental_workflows <- c( + "RMI-PACTA/workflow.pacta", + "RMI-PACTA/workflow.pacta.data.qa", + "RMI-PACTA/workflow.pacta.report" +) + diff --git a/github-repos/github-repos.Rmd b/github-repos/github-repos.Rmd new file mode 100644 index 0000000..5f3bdeb --- /dev/null +++ b/github-repos/github-repos.Rmd @@ -0,0 +1,80 @@ +--- +title: "GitHub repo details" +output: + html_document: + code_folding: hide + toc: true + theme: united +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set( + echo = TRUE, + cache = TRUE +) +``` + +```{r get_repos} + +org <- "RMI-PACTA" +all_repos <- get_repos(org) + +``` + +```{r active_repos} +active_repos <- all_repos |> + lapply( + FUN = function(x) { + if (x[["archived"]] || x[["disabled"]]) { + return(NULL) + } else { + return(x) + } + } + ) +#remove null elements +active_repos <- active_repos[!sapply(active_repos, is.null)] +``` + + +```{r identify_prod_dependencies} + +prod_checked <- lapply(active_repos, prod_checks) + +``` + +```{r identify_prod_essential} + +prod_tibble <- prod_checked |> + lapply( + FUN = function(x) { + pruned <- list( + repo = x[["full_name"]], + codeowners = x[["codeowners"]], + codeowner_errors = x[["codeowner_errors"]], + dep_tree = x[["dep_tree"]], + lifecycle = x[["lifecycle_badge"]], + private = x[["private"]], + updated = x[["updated_at"]], + open_issues = x[["open_issues_count"]], + default_branch = x[["default_branch"]] + ) + clean <- lapply(pruned, function(x) { + if (is.null(x)) { + return(NA) + } else { + return(x) + } + }) + return(clean) + } + ) |> + lapply( + FUN = tibble::as_tibble + ) |> + dplyr::bind_rows() |> + dplyr::arrange(desc(dep_tree), repo) + +prod_tibble + +``` diff --git a/tests/testthat/test-generate_package_table.R b/tests/testthat/test-generate_package_table.R index 8849056..0cc1245 100644 --- a/tests/testthat/test-generate_package_table.R +++ b/tests/testthat/test-generate_package_table.R @@ -1,3 +1,3 @@ test_that("multiplication works", { - expect_equal(2 * 2, 4) + expect_identical(2L * 2L, 4L) }) From 78e3cc7325a7d7eec1d178578a1e85bea6f11882 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 15:35:25 +0100 Subject: [PATCH 02/17] include codeowners in table --- R/codeowners.R | 6 +++++- github-repos/github-repos.Rmd | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/R/codeowners.R b/R/codeowners.R index 587b219..0b5b034 100644 --- a/R/codeowners.R +++ b/R/codeowners.R @@ -1,6 +1,7 @@ get_codeowner <- function( repo_fullname, - path = ".github/CODEOWNERS" # nolint: non_portable_path + path = ".github/CODEOWNERS", # nolint: non_portable_path + format = TRUE ) { raw_content <- tryCatch( expr = { @@ -31,6 +32,9 @@ get_codeowner <- function( replacement = "", x = default_owner ) + if (format) { + default_owner_clean <- paste0("@", default_owner_clean) + } return(default_owner_clean) } if (requireNamespace("memoise")) { diff --git a/github-repos/github-repos.Rmd b/github-repos/github-repos.Rmd index 5f3bdeb..a7f9b7d 100644 --- a/github-repos/github-repos.Rmd +++ b/github-repos/github-repos.Rmd @@ -50,7 +50,7 @@ prod_tibble <- prod_checked |> FUN = function(x) { pruned <- list( repo = x[["full_name"]], - codeowners = x[["codeowners"]], + codeowner = x[["codeowner"]], codeowner_errors = x[["codeowner_errors"]], dep_tree = x[["dep_tree"]], lifecycle = x[["lifecycle_badge"]], From b1b9890181e0f8ba4c68a05c4019c032d8161d21 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 15:47:10 +0100 Subject: [PATCH 03/17] Update prod vs experimental lists --- R/repo_lists.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/repo_lists.R b/R/repo_lists.R index d872fb7..a339848 100644 --- a/R/repo_lists.R +++ b/R/repo_lists.R @@ -1,7 +1,6 @@ prod_workflows <- c( "RMI-PACTA/pactaverse", "RMI-PACTA/workflow.data.preparation", - "RMI-PACTA/workflow.mfm2023", "RMI-PACTA/workflow.portfolio.parsing", "RMI-PACTA/workflow.prepare.pacta.indices", "RMI-PACTA/workflow.scenario.preparation", @@ -16,6 +15,7 @@ r2dii.repos <- c( ) experimental_workflows <- c( + "RMI-PACTA/workflow.mfm2023", "RMI-PACTA/workflow.pacta", "RMI-PACTA/workflow.pacta.data.qa", "RMI-PACTA/workflow.pacta.report" From 571208824fdece687147c5f56447baefb0091b71 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 18:40:38 +0100 Subject: [PATCH 04/17] Format outputs --- github-repos/github-repos.Rmd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/github-repos/github-repos.Rmd b/github-repos/github-repos.Rmd index a7f9b7d..a4e84e8 100644 --- a/github-repos/github-repos.Rmd +++ b/github-repos/github-repos.Rmd @@ -37,7 +37,7 @@ active_repos <- active_repos[!sapply(active_repos, is.null)] ``` -```{r identify_prod_dependencies} +```{r identify_prod_dependencies, include=FALSE} prod_checked <- lapply(active_repos, prod_checks) @@ -57,11 +57,12 @@ prod_tibble <- prod_checked |> private = x[["private"]], updated = x[["updated_at"]], open_issues = x[["open_issues_count"]], + license = x[["license"]][["name"]], default_branch = x[["default_branch"]] ) clean <- lapply(pruned, function(x) { if (is.null(x)) { - return(NA) + return(NA_character_) } else { return(x) } @@ -75,6 +76,6 @@ prod_tibble <- prod_checked |> dplyr::bind_rows() |> dplyr::arrange(desc(dep_tree), repo) -prod_tibble +knitr::kable(prod_tibble) ``` From 3f5d736d099d02459db91450539a8ad701580ba6 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 19:28:15 +0100 Subject: [PATCH 05/17] Refactor extracting file contents --- R/codeowners.R | 23 ++++------------------- R/generate_package_table.R | 35 ++--------------------------------- R/get_gh_file.R | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 52 deletions(-) create mode 100644 R/get_gh_file.R diff --git a/R/codeowners.R b/R/codeowners.R index 0b5b034..596aa9f 100644 --- a/R/codeowners.R +++ b/R/codeowners.R @@ -3,28 +3,13 @@ get_codeowner <- function( path = ".github/CODEOWNERS", # nolint: non_portable_path format = TRUE ) { - raw_content <- tryCatch( - expr = { - raw_content <- gh::gh( - "GET /repos/{repo}/contents/{path}", # nolint: non_portable_path - repo = repo_fullname, - path = path - ) - }, - error = function(e) { - NA_character_ - } - ) - if (identical(raw_content, NA_character_)) { - # Early return - return(NA_character_) + content <- get_gh_text_file(repo_fullname, file_path = path) + if (is.null(content)) { + return(NULL) } - content_b64 <- raw_content[["content"]] - content_char <- rawToChar(base64enc::base64decode(content_b64)) - content_words <- strsplit(content_char, "\n", fixed = TRUE)[[1L]] default_owner <- grep( pattern = "^\\s*\\*\\s+@\\S+", - x = content_words, + x = content, value = TRUE ) default_owner_clean <- gsub( diff --git a/R/generate_package_table.R b/R/generate_package_table.R index 6cb5e97..d7c8a6b 100644 --- a/R/generate_package_table.R +++ b/R/generate_package_table.R @@ -95,7 +95,7 @@ format_maintainer <- function(maintainer) { format_maintainer_v <- Vectorize(format_maintainer) table_lifecycle <- function(repo_path) { - readme <- fetch_readme(repo_path) + readme <- get_gh_text_file(repo_path, file_path = "README.md") if (is.null(readme)) { return(NA_character_) @@ -132,7 +132,7 @@ table_latest_sha <- function(repo_path) { } table_maintainer <- function(repo_path) { - codeowners <- fetch_codeowners(repo_path) + codeowners <- get_gh_text_file(repo_path, file_path = ".github/CODEOWNERS") if (is.null(codeowners)) { return(NA_character_) } @@ -149,22 +149,6 @@ table_maintainer <- function(repo_path) { return(maintainer) } -fetch_codeowners <- function(repo_path) { - response <- tryCatch( - gh::gh( - "/repos/{repo}/contents/.github/CODEOWNERS", - repo = repo_path, - ref = "main" - ), - error = function(cond) return(NULL) - ) - - if (!is.null(response)) { - return(readLines(response$download_url)) - } - return(NULL) -} - fetch_main_sha <- function(repo_path) { response <- tryCatch( gh::gh( @@ -179,18 +163,3 @@ fetch_main_sha <- function(repo_path) { } return(NULL) } - -fetch_readme <- function(repo_path) { - response <- tryCatch( - gh::gh( - "/repos/{repo}/contents/README.md", - repo = repo_path - ), - error = function(cond) return(NULL) - ) - - if (!is.null(response)) { - return(readLines(response$download_url)) - } - return(NULL) -} diff --git a/R/get_gh_file.R b/R/get_gh_file.R new file mode 100644 index 0000000..28431cb --- /dev/null +++ b/R/get_gh_file.R @@ -0,0 +1,34 @@ +get_gh_text_file <- function(repo_fullname, file_path) { + response <- tryCatch( + expr = { + gh::gh( + "/repos/{repo_fullname}/contents/{file_path}", # nolint: non_portable_path + repo_fullname = repo_fullname, + file_path = file_path + ) + }, + error = function(cond) return(NULL) + ) + if (is.null(response)) { + out <- NULL + } else if (response[["content"]] == "") { + # files larger than 1MB are not embedded in content + out <- readLines( + response[["download_url"]] + ) + } else { + out <- strsplit( + x = rawToChar( + base64enc::base64decode( + response[["content"]] + ) + ), + split = "\n", + fixed = TRUE + )[[1L]] + } + return(out) +} +if (requireNamespace("memoise")) { + get_gh_text_file <- memoise::memoise(get_gh_text_file) +} From 1336ffe200f7bd848eba30231197885862dfb13b Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 27 Mar 2024 21:14:10 +0100 Subject: [PATCH 06/17] Add check for workflows --- R/get_gh_file.R | 27 +++++++++ R/prod_checks.R | 4 ++ R/workflows.R | 102 ++++++++++++++++++++++++++++++++++ github-repos/github-repos.Rmd | 1 + 4 files changed, 134 insertions(+) create mode 100644 R/workflows.R diff --git a/R/get_gh_file.R b/R/get_gh_file.R index 28431cb..35c308f 100644 --- a/R/get_gh_file.R +++ b/R/get_gh_file.R @@ -32,3 +32,30 @@ get_gh_text_file <- function(repo_fullname, file_path) { if (requireNamespace("memoise")) { get_gh_text_file <- memoise::memoise(get_gh_text_file) } + +get_gh_dir_listing <- function(repo_fullname, dir_path) { + response <- tryCatch( + expr = { + gh::gh( + "/repos/{repo_fullname}/contents/{dir_path}", # nolint: non_portable_path + repo_fullname = repo_fullname, + dir_path = dir_path + ) + }, + error = function(cond) return(NULL) + ) + if (is.null(response)) { + out <- NULL + } else { + out <- lapply( + response, + function(x) { + list( + name = x[["name"]], + path = x[["path"]] + ) + } + ) + } + return(out) +} diff --git a/R/prod_checks.R b/R/prod_checks.R index 56233b0..0b3be9b 100644 --- a/R/prod_checks.R +++ b/R/prod_checks.R @@ -19,6 +19,10 @@ prod_checks <- function(repo_json) { lifecycle_badge <- format_lifecycle(table_lifecycle(repo_json[["full_name"]])) repo_json[["lifecycle_badge"]] <- lifecycle_badge } + if (is.null(repo_json[["workflows"]])) { + workflows <- workflow_summary(repo_json[["full_name"]]) + repo_json[["workflows"]] <- workflows + } return(repo_json) } if (requireNamespace("memoise")) { diff --git a/R/workflows.R b/R/workflows.R new file mode 100644 index 0000000..6f1d36f --- /dev/null +++ b/R/workflows.R @@ -0,0 +1,102 @@ +get_workflows <- function(repo_fullname) { + listing <- get_gh_dir_listing( + repo_fullname, + dir_path = ".github/workflows" # nolint: non_portable_path + ) + + contents <- list() + for (i in seq_along(listing)) { + file_content <- get_gh_text_file(repo_fullname, listing[[i]][["path"]]) + filename <- listing[[i]][["name"]] + contents[filename] <- list(file_content) + } + return(contents) +} +if (requireNamespace("memoise")) { + get_workflows <- memoise::memoise(get_workflows) +} + +parse_workflow <- function(workflow) { + workflow_list <- yaml::yaml.load(workflow) + + # {yaml} interprets `on:` as `TRUE` + triggers <- workflow_list[[TRUE]] + jobs <- workflow_list[["jobs"]] + rmi_actions <- vapply( + X = jobs, + FUN = function(job) { + if (is.null(job[["uses"]])) { + return(NULL) + } else { + return( + grep( + pattern = "RMI-PACTA/actions", # nolint: non_portable_path + x = job[["uses"]], + value = TRUE + ) + ) + } + }, + FUN.VALUE = character(1L) + ) + uses_rmi_actions <- !all(is.null(rmi_actions)) + return( + list( + triggers = triggers, + rmi_actions = rmi_actions, + uses_rmi_actions = uses_rmi_actions + ) + ) +} + +check_workflows <- function(repo_fullname) { + workflow_definitions <- get_workflows(repo_fullname) + lapply( + workflow_definitions, + parse_workflow + ) +} + +workflow_summary <- function( + repo_fullname, + expected = c( + "R.yml", + "docker.yml" + ) +) { + workflows <- check_workflows(repo_fullname) + workflow_summary <- list() + for (wf in expected) { + if (is.null(workflows[wf])) { + is_expected <- FALSE + details <- NULL + } else { + checks_main <- "main" %in% workflows[[wf]][["triggers"]][["push"]] + checks_prs <- "pull_request" %in% names(workflows[[wf]][["triggers"]]) + standard_checks <- checks_main && checks_prs + uses_correct_rmi_actions <- grepl( + pattern = wf, + x = workflows[[wf]][["rmi_actions"]], + fixed = TRUE + ) + is_expected <- standard_checks && uses_correct_rmi_actions + details <- list( + checks_main = checks_main, + checks_prs = checks_prs, + standard_checks = standard_checks, + uses_correct_rmi_actions = uses_correct_rmi_actions + ) + } + workflow_summary[[wf]] <- list( + standard = is_expected, + details = details + ) + } + workflow_summary[["all_standard"]] <- all( + sapply( + workflow_summary, + function(x) x[["standard"]] + ) + ) + return(workflow_summary) +} diff --git a/github-repos/github-repos.Rmd b/github-repos/github-repos.Rmd index a4e84e8..b4a305d 100644 --- a/github-repos/github-repos.Rmd +++ b/github-repos/github-repos.Rmd @@ -54,6 +54,7 @@ prod_tibble <- prod_checked |> codeowner_errors = x[["codeowner_errors"]], dep_tree = x[["dep_tree"]], lifecycle = x[["lifecycle_badge"]], + workflows = x[["workflows"]], private = x[["private"]], updated = x[["updated_at"]], open_issues = x[["open_issues_count"]], From 4a98317ca02ee76f356d7c7b229eda7cef844b68 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 28 Mar 2024 11:57:01 +0100 Subject: [PATCH 07/17] Add function to check rulesets --- R/rulesets.R | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ R/utils.R | 12 +++++++ 2 files changed, 110 insertions(+) create mode 100644 R/rulesets.R create mode 100644 R/utils.R diff --git a/R/rulesets.R b/R/rulesets.R new file mode 100644 index 0000000..70d6a26 --- /dev/null +++ b/R/rulesets.R @@ -0,0 +1,98 @@ +get_rulesets <- function(repo_fullname) { + response <- tryCatch( + expr = { + gh::gh( + "/repos/{repo_fullname}/rulesets", # nolint: non_portable_path + repo_fullname = repo_fullname + ) + }, + error = function(cond) return(NULL) + ) + if (is.null(response)) { + out <- NULL + } else { + out <- vapply( + X = response, + FUN = function(ruleset) { + get_ruleset_contents(repo_fullname, ruleset[["id"]]) + }, + FUN.VALUE = list(1L) + ) + } + return(out) +} +if (requireNamespace("memoise")) { + get_rulesets <- memoise::memoise(get_rulesets) +} + +get_ruleset_contents <- function(repo_fullname, ruleset_id) { + response <- tryCatch( + expr = { + gh::gh( + "/repos/{repo_fullname}/rulesets/{ruleset_id}", # nolint: non_portable_path + repo_fullname = repo_fullname, + ruleset_id = ruleset_id + ) + }, + error = function(cond) return(NULL) + ) + if (is.null(response)) { + out <- NULL + } else { + out <- list( + list( + id = response[["id"]], + name = response[["name"]], + target = response[["target"]], + enforcement = response[["enforcement"]], + conditions = response[["conditions"]], + rules = response[["rules"]] + ) + ) + } + return(out) +} +if (requireNamespace("memoise")) { + get_ruleset_contents <- memoise::memoise(get_ruleset_contents) +} + +check_actions_rulesets <- function(repo_fullname) { + ruleset_repo <- "RMI-PACTA/actions" # nolint: non_portable_path + rulesets <- get_rulesets(repo_fullname) + if (is.null(rulesets)) { + return(NULL) + } + expected_rules <- get_gh_dir_listing( + repo_fullname = ruleset_repo, + "rulesets" + ) + expected <- list() + for (i in seq_along(expected_rules)) { + file_content <- get_gh_text_file( + repo_fullname = ruleset_repo, + expected_rules[[i]][["path"]] + ) + filename <- expected_rules[[i]][["name"]] + expected[filename] <- list( + jsonlite::fromJSON( + txt = file_content, + simplifyDataFrame = FALSE, + simplifyVector = FALSE + ) + ) + } + enabled_rulesets <- vapply( + X = expected, + FUN = function(expected_ruleset) { + any(vapply( + X = rulesets, + FUN = function(ruleset) { + list_is_subset(expected_ruleset, ruleset) + }, + FUN.VALUE = logical(1L) + )) + }, + FUN.VALUE = logical(1L) + ) + return(as.list(enabled_rulesets)) +} diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 0000000..40aef98 --- /dev/null +++ b/R/utils.R @@ -0,0 +1,12 @@ +# check if all elements of x are in y, recursively +list_is_subset <- function(x, y) { + if (is.list(x) && is.list(y)) { + all(vapply(x, function(x_elem) { + any(vapply(y, function(y_elem) { + list_is_subset(x_elem, y_elem) + }, logical(1))) + }, logical(1))) + } else { + identical(x, y) + } +} From 2bd501ca7cc1ddea2581cc2e2753698727a29f22 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 28 Mar 2024 12:16:05 +0100 Subject: [PATCH 08/17] Fix bug in workflows check --- R/prod_checks.R | 4 ++++ R/workflows.R | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/R/prod_checks.R b/R/prod_checks.R index 0b3be9b..32dba8a 100644 --- a/R/prod_checks.R +++ b/R/prod_checks.R @@ -23,6 +23,10 @@ prod_checks <- function(repo_json) { workflows <- workflow_summary(repo_json[["full_name"]]) repo_json[["workflows"]] <- workflows } + if (is.null(repo_json[["enabled_rules"]])) { + enabled_rules <- check_actions_rulesets(repo_json[["full_name"]]) + repo_json[["enabled_rules"]] <- enabled_rules + } return(repo_json) } if (requireNamespace("memoise")) { diff --git a/R/workflows.R b/R/workflows.R index 6f1d36f..6b378bb 100644 --- a/R/workflows.R +++ b/R/workflows.R @@ -26,7 +26,7 @@ parse_workflow <- function(workflow) { X = jobs, FUN = function(job) { if (is.null(job[["uses"]])) { - return(NULL) + return(NA_character_) } else { return( grep( From 3a2a7838a02ca4c97169e39d6c77f937abeb6efb Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 28 Mar 2024 14:34:07 +0100 Subject: [PATCH 09/17] Add more handling for edge cases --- R/prod_checks.R | 21 ++++++++++++++++++++- R/workflows.R | 23 ++++++++++++++++------- github-repos/github-repos.Rmd | 16 +++++++++++++++- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/R/prod_checks.R b/R/prod_checks.R index 32dba8a..b130309 100644 --- a/R/prod_checks.R +++ b/R/prod_checks.R @@ -16,9 +16,28 @@ prod_checks <- function(repo_json) { repo_json[["dep_tree"]] <- dep_tree } if (is.null(repo_json[["lifecycle_badge"]])) { - lifecycle_badge <- format_lifecycle(table_lifecycle(repo_json[["full_name"]])) + lifecycle_badge <- format_lifecycle( + table_lifecycle(repo_json[["full_name"]]) + ) repo_json[["lifecycle_badge"]] <- lifecycle_badge } + if (is.null(repo_json[["has_docker"]])) { + repo_json[["has_docker"]] <- !is.null( + get_gh_text_file(repo_json[["full_name"]], "Dockerfile") + ) + } + if (is.null(repo_json[["has_R"]])) { + repo_json[["has_R"]] <- !is.null( + get_gh_dir_listing(repo_json[["full_name"]], "R") + ) + } + expected_workflows <- NULL + if (repo_json[["has_R"]]) { + expected_workflows <- c(expected_workflows, "R.yml") + } + if (repo_json[["has_docker"]]) { + expected_workflows <- c(expected_workflows, "docker.yml") + } if (is.null(repo_json[["workflows"]])) { workflows <- workflow_summary(repo_json[["full_name"]]) repo_json[["workflows"]] <- workflows diff --git a/R/workflows.R b/R/workflows.R index 6b378bb..4dc1cb8 100644 --- a/R/workflows.R +++ b/R/workflows.R @@ -28,13 +28,15 @@ parse_workflow <- function(workflow) { if (is.null(job[["uses"]])) { return(NA_character_) } else { - return( - grep( - pattern = "RMI-PACTA/actions", # nolint: non_portable_path - x = job[["uses"]], - value = TRUE - ) + out <- grep( + pattern = "RMI-PACTA/actions", # nolint: non_portable_path + x = job[["uses"]], + value = TRUE ) + if (length(out) == 0L) { + return(NA_character_) + } + return(out) } }, FUN.VALUE = character(1L) @@ -71,7 +73,14 @@ workflow_summary <- function( is_expected <- FALSE details <- NULL } else { - checks_main <- "main" %in% workflows[[wf]][["triggers"]][["push"]] + if ( + !("push" %in% names(workflows[[wf]][["triggers"]])) || + is.null(workflows[[wf]][["triggers"]][["push"]]) + ) { + checks_main <- FALSE + } else { + checks_main <- "main" %in% workflows[[wf]][["triggers"]][["push"]] + } checks_prs <- "pull_request" %in% names(workflows[[wf]][["triggers"]]) standard_checks <- checks_main && checks_prs uses_correct_rmi_actions <- grepl( diff --git a/github-repos/github-repos.Rmd b/github-repos/github-repos.Rmd index b4a305d..21ee8cd 100644 --- a/github-repos/github-repos.Rmd +++ b/github-repos/github-repos.Rmd @@ -54,7 +54,8 @@ prod_tibble <- prod_checked |> codeowner_errors = x[["codeowner_errors"]], dep_tree = x[["dep_tree"]], lifecycle = x[["lifecycle_badge"]], - workflows = x[["workflows"]], + standard_workflows = x[["workflows"]][["all_standard"]], + main_protected = x[["enabled_rules"]][["protect-main.json"]], private = x[["private"]], updated = x[["updated_at"]], open_issues = x[["open_issues_count"]], @@ -80,3 +81,16 @@ prod_tibble <- prod_checked |> knitr::kable(prod_tibble) ``` + +```{r nice_output} + +prod_tibble |> + dplyr::select(-lifecycle) |> + gt::gt() |> + gt::opt_interactive( + use_pagination = FALSE, + use_filter = TRUE, + use_search = TRUE + ) + +``` From 750f7e5309eb47aec20a0556d7a8d46c86836320 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:16:38 +0200 Subject: [PATCH 10/17] update DESCRIPTION with deps --- DESCRIPTION | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7b3ba48..1dbe0fc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,9 +14,13 @@ Suggests: testthat (>= 3.0.0) Config/testthat/edition: 3 Imports: + base64enc, dplyr, gh, glue, + jsonlite, + pak, purrr, rlang, - tibble + tibble, + yaml From 77279855c93a8e36e90aa9d99811ffbd79ed2cdd Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:17:25 +0200 Subject: [PATCH 11/17] Update RbuildIgnore --- .Rbuildignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.Rbuildignore b/.Rbuildignore index 6bea544..e472355 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,3 +4,4 @@ ^assets$ ^README\.Rmd$ ^\.github$ +^\.lintr$ From 949845cf5c9c4806609afa43202da61a7b9dc898 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:19:41 +0200 Subject: [PATCH 12/17] move file --- {github-repos => inst/extdata}/github-repos.Rmd | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {github-repos => inst/extdata}/github-repos.Rmd (100%) diff --git a/github-repos/github-repos.Rmd b/inst/extdata/github-repos.Rmd similarity index 100% rename from github-repos/github-repos.Rmd rename to inst/extdata/github-repos.Rmd From 6b773d0ad889ce6bc3499e14312d8fbd01619d66 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:20:09 +0200 Subject: [PATCH 13/17] Increment version number to 0.0.0.9003 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1dbe0fc..1e91cc2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: pacta.sit.rep Title: What the Package Does (One Line, Title Case) -Version: 0.0.0.9002 +Version: 0.0.0.9003 Authors@R: person("Jackson", "Hoffart", , "jackson.hoffart@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-8600-5042")) From cfc4da0fd076d3587b19d19c0786d8a15cae0360 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:37:09 +0200 Subject: [PATCH 14/17] linting --- .lintr | 1 + R/dependencies.R | 8 +++++--- R/get_gh_file.R | 4 ++-- R/utils.R | 4 ++-- R/workflows.R | 9 +++++---- inst/extdata/github-repos.Rmd | 9 ++++++--- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.lintr b/.lintr index 9ae5123..90bd541 100644 --- a/.lintr +++ b/.lintr @@ -1,6 +1,7 @@ linters: all_linters() exclusions: list( "tests/testthat.R", + "tests/testthat/test-generate_package_table.R", "R/generate_package_table.R", "R/repo_lists.R" = c( nonportable_path_linter = Inf diff --git a/R/dependencies.R b/R/dependencies.R index 798c094..d0191a7 100644 --- a/R/dependencies.R +++ b/R/dependencies.R @@ -1,6 +1,8 @@ identify_gh_deps <- function(repo_fullname) { deps <- tryCatch( - expr = {pak::pkg_deps(repo_fullname)}, + expr = { + pak::pkg_deps(repo_fullname) + }, error = function(e) { NULL } @@ -38,8 +40,8 @@ identify_if_dep <- function( for (i in seq_along(dep_lists)) { if ( repo_fullname %in% dep_lists[[i]] || - repo_fullname %in% repos_to_check[[i]] - ) { + repo_fullname %in% repos_to_check[[i]] + ) { dep_levels <- c(names(dep_lists)[i], dep_levels) } } diff --git a/R/get_gh_file.R b/R/get_gh_file.R index 35c308f..a8028c0 100644 --- a/R/get_gh_file.R +++ b/R/get_gh_file.R @@ -51,8 +51,8 @@ get_gh_dir_listing <- function(repo_fullname, dir_path) { response, function(x) { list( - name = x[["name"]], - path = x[["path"]] + name = x[["name"]], + path = x[["path"]] ) } ) diff --git a/R/utils.R b/R/utils.R index 40aef98..ef30df9 100644 --- a/R/utils.R +++ b/R/utils.R @@ -4,8 +4,8 @@ list_is_subset <- function(x, y) { all(vapply(x, function(x_elem) { any(vapply(y, function(y_elem) { list_is_subset(x_elem, y_elem) - }, logical(1))) - }, logical(1))) + }, logical(1L))) + }, logical(1L))) } else { identical(x, y) } diff --git a/R/workflows.R b/R/workflows.R index 4dc1cb8..0afb512 100644 --- a/R/workflows.R +++ b/R/workflows.R @@ -76,7 +76,7 @@ workflow_summary <- function( if ( !("push" %in% names(workflows[[wf]][["triggers"]])) || is.null(workflows[[wf]][["triggers"]][["push"]]) - ) { + ) { checks_main <- FALSE } else { checks_main <- "main" %in% workflows[[wf]][["triggers"]][["push"]] @@ -102,9 +102,10 @@ workflow_summary <- function( ) } workflow_summary[["all_standard"]] <- all( - sapply( - workflow_summary, - function(x) x[["standard"]] + vapply( + X = workflow_summary, + FUN = function(x) x[["standard"]], + FUN.VALUE = logical(1L) ) ) return(workflow_summary) diff --git a/inst/extdata/github-repos.Rmd b/inst/extdata/github-repos.Rmd index 21ee8cd..3a22d28 100644 --- a/inst/extdata/github-repos.Rmd +++ b/inst/extdata/github-repos.Rmd @@ -8,7 +8,7 @@ output: --- ```{r setup, include=FALSE} -knitr::opts_chunk$set( +knitr::opts_chunk[["set"]]( echo = TRUE, cache = TRUE ) @@ -33,7 +33,11 @@ active_repos <- all_repos |> } ) #remove null elements -active_repos <- active_repos[!sapply(active_repos, is.null)] +active_repos <- active_repos[!vapply( + X = active_repos, + FUN = is.null, + FUN.VALUE = logical(1L) +)] ``` @@ -92,5 +96,4 @@ prod_tibble |> use_filter = TRUE, use_search = TRUE ) - ``` From 0a4707eb4ff7976e8678e36081bb2e1484a18247 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Tue, 9 Apr 2024 17:40:04 +0200 Subject: [PATCH 15/17] Add suggest dependency --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 1e91cc2..95934b8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,6 +10,7 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.1 Suggests: + gt, memoise, testthat (>= 3.0.0) Config/testthat/edition: 3 From abe689b43d2776810e5fdd400fa1f42535860830 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 10 Apr 2024 12:09:58 +0200 Subject: [PATCH 16/17] Move codeowners --- .github/{workflows => }/CODEOWNERS | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/CODEOWNERS (100%) diff --git a/.github/workflows/CODEOWNERS b/.github/CODEOWNERS similarity index 100% rename from .github/workflows/CODEOWNERS rename to .github/CODEOWNERS From ab3be6b885652710d19dd904d83d2b5d004d2a22 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Wed, 10 Apr 2024 12:16:22 +0200 Subject: [PATCH 17/17] Add caches and outputs to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5b6a065..2064942 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .Rhistory .RData .Ruserdata +inst/extdata/*_cache +inst/extdata/*.html +!inst/extdata/*.[Rr][Mm][Dd]