diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 52c2b27..28a665f 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -25,7 +25,7 @@ jobs: - {os: ubuntu-latest, r: 'oldrel-1'} env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PAT: ${{ secrets.GIST_TOKEN }} R_KEEP_PKG_SOURCE: yes steps: @@ -44,6 +44,12 @@ jobs: extra-packages: any::rcmdcheck needs: check + # Added step for testing gist creation using GIST_TOKEN + # - name: Run gist creation test + # run: | + # echo "Running gist creation test..." + # Rscript -e "gistr::gist_create(file = 'tests/test-gist.md', description = 'Test gist creation')" + - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true diff --git a/DESCRIPTION b/DESCRIPTION index ad7af34..d3ada32 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,12 +12,12 @@ Description: Work with 'GitHub' 'gists' from 'R' (e.g., . Version: 0.9.0.93 Authors@R: c( - person("Scott", "Chamberlain", role = c("aut", "cre"), - email = "myrmecocystus@gmail.com", - comment = c(ORCID="0000-0003-1444-9135")), + person("Scott", "Chamberlain", , "myrmecocystus@gmail.com", role = "aut", + comment = c(ORCID = "0000-0003-1444-9135")), person("Ramnath", "Vaidyanathan", role = "aut"), person("Karthik", "Ram", role = "aut"), - person("Milgram", "Eric", role = "aut") + person("Milgram", "Eric", role = c("aut", "cre")), + person("Eric R.", "Scott", role = "ctb") ) License: MIT + file LICENSE URL: https://github.com/ropensci/gistr (devel), @@ -35,11 +35,12 @@ Imports: assertthat, knitr, rmarkdown, - dplyr + dplyr, + gitcreds Suggests: git2r, testthat -RoxygenNote: 7.1.1 +RoxygenNote: 7.3.2 X-schema.org-applicationCategory: Web X-schema.org-keywords: http, https, API, web-services, GitHub, GitHub API, gist, gists, code, script, snippet X-schema.org-isPartOf: https://ropensci.org diff --git a/NEWS.md b/NEWS.md index 70bd95d..05d6ecb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ gistr in development =============== +### NEW FEATURES + +* `gistr` now uses `gitcreds` to discover GitHub PATs and now recommends you store your PAT using `gitcreds::gitcreds_set()` rather than as the `GITHUB_PAT` environment variable (although this still works). + ### BUG FIXES * fix `gist_auth()`: at some point `httr::oauth2.0_token` stopped returning the token in the `headers` slot; can't figure out when this change happened; fix is to get the token from a different place in the returned object; changes to `gist_auth()` to access that new path to the token (#83) diff --git a/R/gist_auth.R b/R/gist_auth.R index bbb54df..34cc7aa 100644 --- a/R/gist_auth.R +++ b/R/gist_auth.R @@ -1,22 +1,22 @@ #' Authorize with GitHub. #' -#' This function is run automatically to allow gistr to access your GitHub +#' This function is run automatically to allow gistr to access your GitHub #' account. #' #' There are two ways to authorise gistr to work with your GitHub account: -#' -#' - Generate a personal access token with the gist scope selected, and set it -#' as the `GITHUB_PAT` environment variable per session using `Sys.setenv` -#' or across sessions by adding it to your `.Renviron` file or similar. -#' See +#' +#' - Generate a personal access token with the gist scope selected, and store it +#' with `gitcreds::gitcreds_set()`. Alternatively you can set it as the +#' `GITHUB_PAT` environment variable per session using `Sys.setenv()` or across +#' sessions by adding it to your `.Renviron` file or similar. See #' https://help.github.com/articles/creating-an-access-token-for-command-line-use -#' for help -#' - Interactively login into your GitHub account and authorise with OAuth. +#' for help or use `usethis::create_github_token()`. +#' - Interactively log in into your GitHub account and authorise with OAuth. #' -#' Using `GITHUB_PAT` is recommended. +#' Using `gitcreds::gitcreds_set()` is recommended. #' #' @export -#' @param app An [httr::oauth_app()] for GitHub. The default uses an +#' @param app An [httr::oauth_app()] for GitHub. The default uses an #' application `gistr_oauth` created by Scott Chamberlain. #' @param reauth (logical) Force re-authorization? #' @return a named list, with a single slot for `Authorization`, with a single @@ -27,24 +27,40 @@ #' } gist_auth <- function(app = gistr_app, reauth = FALSE) { - + + #if there is a token cached, use that if (exists("auth_config", envir = cache) && !reauth) { - return(auth_header(cache$auth_config$auth_token$credentials$access_token)) + return(auth_header(cache$auth_config$auth_token)) } - pat <- Sys.getenv("GITHUB_PAT", "") - if (!identical(pat, "")) { - auth_config <- list(auth_token=list(credentials=list(access_token=pat))) - } else if (!interactive()) { - stop("In non-interactive environments, please set GITHUB_PAT env to a GitHub", - " access token (https://help.github.com/articles/creating-an-access-token-for-command-line-use)", - call. = FALSE) - } else { - endpt <- httr::oauth_endpoints("github") - token <- httr::oauth2.0_token(endpt, app, scope = "gist", cache = !reauth) - auth_config <- httr::config(token = token) + #if nothing cached, use gitcreds to retrieve PAT stored as an GITHUB_PAT + #environment variable or set with gitcreds::gitcreds_set(). gitcreds_get() + #errors when no PAT is found, but we want to try one more method, so silence + #this error and return NULL. + creds <- tryCatch( + error = function(cnd) { + return(NULL) + }, + gitcreds::gitcreds_get() + ) + token <- creds$password + #TODO would be great to check here that token has "gist" scope + #if no token, or invalid token and interactive, try direct oauth + if ((is.null(token) | !valid_gh_pat(token))) { + if (interactive()) { + endpt <- httr::oauth_endpoints("github") + auth <- httr::oauth2.0_token(endpt, app, scope = "gist", cache = !reauth) + token <- auth$credentials$access_token + } else { + stop("In non-interactive environments, please set GITHUB_PAT env to a GitHub", + " access token (https://help.github.com/articles/creating-an-access-token-for-command-line-use)", + call. = FALSE) + } } + + #cache auth config + auth_config <- httr::config(token = token) cache$auth_config <- auth_config - auth_header(auth_config$auth_token$credentials$access_token) + return(auth_header(auth_config$auth_token)) } auth_header <- function(x) list(Authorization = paste0("token ", x)) @@ -56,3 +72,11 @@ gistr_app <- httr::oauth_app( "89ecf04527f70e0f9730", "77b5970cdeda925513b2cdec40c309ea384b74b7" ) + +# inspired by https://github.com/r-lib/gh/blob/main/R/gh_token.R +valid_gh_pat <- function(x) { + !is.null(x) & ( + grepl("^(gh[pousr]_[A-Za-z0-9_]{36,251}|github_pat_[A-Za-z0-9_]{36,244})$", x) || + grepl("^[[:xdigit:]]{40}$", x) + ) +} diff --git a/R/gistr-package.R b/R/gistr-package.R index de9d669..43b1abe 100644 --- a/R/gistr-package.R +++ b/R/gistr-package.R @@ -22,13 +22,12 @@ #' @importFrom jsonlite fromJSON flatten #' @name gistr-package #' @aliases gistr -#' @docType package #' @title R client for GitHub gists #' @author Scott Chamberlain \email{myrmecocystus@@gmail.com} #' @author Ramnath Vaidyanathan \email{ramnath.vaidya@@gmail.com} #' @author Karthik Ram \email{karthik.ram@@gmail.com} #' @keywords package -NULL +"_PACKAGE" #' @title Create gists #' @@ -43,7 +42,7 @@ NULL #' - [gist_create_git()] - Create gists from files or code #' blocks, using git. Because this function uses git, you have more #' flexibility than with the above function: you can include any binary files, -#' and can easily upload all artifacts. +#' and can easily upload all artifacts. #' - [gist_create_obj()] - Create gists from R objects: data.frame, list, #' character string, matrix, or numeric. Uses the GitHub HTTP API. #' diff --git a/man/gist_auth.Rd b/man/gist_auth.Rd index 7f73c13..67e95bd 100644 --- a/man/gist_auth.Rd +++ b/man/gist_auth.Rd @@ -24,16 +24,16 @@ account. \details{ There are two ways to authorise gistr to work with your GitHub account: \itemize{ -\item Generate a personal access token with the gist scope selected, and set it -as the \code{GITHUB_PAT} environment variable per session using \code{Sys.setenv} -or across sessions by adding it to your \code{.Renviron} file or similar. -See +\item Generate a personal access token with the gist scope selected, and store it +with \code{gitcreds::gitcreds_set()}. Alternatively you can set it as the +\code{GITHUB_PAT} environment variable per session using \code{Sys.setenv()} or across +sessions by adding it to your \code{.Renviron} file or similar. See https://help.github.com/articles/creating-an-access-token-for-command-line-use -for help -\item Interactively login into your GitHub account and authorise with OAuth. +for help or use \code{usethis::create_github_token()}. +\item Interactively log in into your GitHub account and authorise with OAuth. } -Using \code{GITHUB_PAT} is recommended. +Using \code{gitcreds::gitcreds_set()} is recommended. } \examples{ \dontrun{ diff --git a/man/gistr-package.Rd b/man/gistr-package.Rd index 8cf9feb..6adc362 100644 --- a/man/gistr-package.Rd +++ b/man/gistr-package.Rd @@ -21,6 +21,15 @@ and record it in the \code{GITHUB_PAT} envar. } Using the \code{GITHUB_PAT} is recommended. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/ropensci/gistr (devel)} + \item \url{https://docs.ropensci.org/gistr (website)} + \item Report bugs at \url{https://github.com/ropensci/gistr/issues} +} + } \author{ Scott Chamberlain \email{myrmecocystus@gmail.com} diff --git a/tests/testthat/.gitignore b/tests/testthat/.gitignore new file mode 100644 index 0000000..b49c113 --- /dev/null +++ b/tests/testthat/.gitignore @@ -0,0 +1 @@ +.httr-oauth diff --git a/tests/testthat/test-gist_auth.R b/tests/testthat/test-gist_auth.R new file mode 100644 index 0000000..c7ec666 --- /dev/null +++ b/tests/testthat/test-gist_auth.R @@ -0,0 +1,21 @@ +test_that("gitcreds finds PAT", { + expect_equal(gitcreds::gitcreds_get()$password, Sys.getenv("GITHUB_PAT")) +}) + + +test_that("gist_auth finds PAT", { + skip_on_cran() + auth <- gist_auth(reauth = TRUE) + expect_equal( + auth$Authorization, + paste0("token ", gitcreds::gitcreds_get()$password) + ) +}) + +#can't (easily) test PAT stored with gitcreds::gitcreds_set() or interactive oauth + +test_that("valid_gh_pat() works", { + expect_false(valid_gh_pat("hello")) + expect_false(valid_gh_pat("")) + expect_false(valid_gh_pat(NULL)) +})