From 2df76643d293dc6fae43938abfa32cd4c8336d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Tue, 26 May 2026 11:27:59 +0200 Subject: [PATCH] Only warn for unrecognized token formats GitHub PAT format validation now issues a warning by default instead of throwing an error, so a PAT in an unrecognized (e.g. newly introduced) format is still used. Set the `gh_validate_tokens` option or the `GH_VALIDATE_TOKENS` environment to `"off"`, `"warn"` or `"error"` to configure this. Related to #231. --- .Rbuildignore | 1 + .gitignore | 1 + NEWS.md | 6 +++ R/gh_token.R | 72 ++++++++++++++++++++++++++---- README.Rmd | 7 ++- README.md | 23 ++++++---- man/gh_token.Rd | 11 +++++ tests/testthat/_snaps/gh_whoami.md | 2 +- tests/testthat/test-gh_token.R | 39 ++++++++++++++++ 9 files changed, 143 insertions(+), 19 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 5688c53..7657876 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -19,3 +19,4 @@ ^LICENSE\.md$ ^[\.]?air\.toml$ ^\.vscode$ +^[.]dev$ diff --git a/.gitignore b/.gitignore index c0f5da0..f75d201 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /revdep docs inst/doc +/.dev diff --git a/NEWS.md b/NEWS.md index d4d52d3..a012830 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # gh (development version) +* GitHub PAT format validation now issues a warning by default instead of + throwing an error, so a PAT in an unrecognized (e.g. newly introduced) + format is still used. Set the `gh_validate_tokens` option or the + `GH_VALIDATE_TOKENS` environment to `"off"`, `"warn"` or `"error"` + to configure this. + # gh 1.5.0 ## BREAKING CHANGES diff --git a/R/gh_token.R b/R/gh_token.R index f00ef51..4f02c34 100644 --- a/R/gh_token.R +++ b/R/gh_token.R @@ -25,6 +25,17 @@ #' point on, gh (via [gitcreds::gitcreds_get()]) should be able to find it #' without further effort on your part. #' +#' # Token format validation +#' +#' gh warns if the PAT it retrieves does not match a known format. +#' Set `options(gh_validate_tokens = "off")` or the +#' `GH_VALIDATE_TOKENS=off` environment variable to avoid this warning. +#' The option takes precedence over the environment variable. +#' +#' Set `options(gh_validate_tokens = "error")` or the +#' `GH_VALIDATE_TOKENS=error` environment variable to make gh throw an +#' error for an unrecognized PAT format. +#' #' @param api_url GitHub API URL. Defaults to the `GITHUB_API_URL` environment #' variable, if set, and otherwise to . #' @@ -90,6 +101,10 @@ new_gh_pat <- function(x) { # validates PAT only in a very narrow, technical, and local sense validate_gh_pat <- function(x) { stopifnot(inherits(x, "gh_pat")) + mode <- get_validate_tokens_mode() + if (mode == "off") { + return(x) + } if ( x == "" || # https://github.blog/changelog/2021-03-04-authentication-token-format-updates/ @@ -101,18 +116,59 @@ validate_gh_pat <- function(x) { ) || grepl("^[[:xdigit:]]{40}$", x) ) { - x + return(x) + } + url <- "https://gh.r-lib.org/articles/managing-personal-access-tokens.html" + msg <- c( + "Invalid GitHub PAT format", + "i" = "A GitHub PAT must have one of three forms:", + "*" = "40 hexadecimal digits (older PATs)", + "*" = "A 'ghp_' prefix followed by 36 to 251 more characters (newer + PATs)", + "*" = "A 'github_pat_' prefix followed by 36 to 244 more characters + (fine-grained PATs)", + "i" = "Read more at {.url {url}}." + ) + sil <- c( + "i" = "Set {.code options(gh_validate_tokens = \"off\")} or env var + {.envvar GH_VALIDATE_TOKENS=off} to silence this." + ) + if (mode == "warn") { + cli::cli_warn( + c(msg, sil), + .frequency = "regularly", + .frequency_id = "gh_invalid_pat" + ) + return(x) + } + cli::cli_abort(msg) +} + +# Resolve token-validation mode: option > env var > "warn". +get_validate_tokens_mode <- function() { + opt <- getOption("gh_validate_tokens") + if (is.null(opt)) { + env <- Sys.getenv("GH_VALIDATE_TOKENS", unset = "") + mode <- if (nzchar(env)) env else "warn" } else { - url <- "https://gh.r-lib.org/articles/managing-personal-access-tokens.html" + mode <- opt + } + if (!is.character(mode) || length(mode) != 1L || is.na(mode)) { + cli::cli_abort(c( + "Invalid token validation setting: must be a single string.", + "i" = "Got {.obj_type_friendly {mode}}." + )) + } + mode <- tolower(mode) + if (!mode %in% c("off", "warn", "error")) { cli::cli_abort(c( - "Invalid GitHub PAT format", - "i" = "A GitHub PAT must have one of three forms:", - "*" = "40 hexadecimal digits (older PATs)", - "*" = "A 'ghp_' prefix followed by 36 to 251 more characters (newer PATs)", - "*" = "A 'github_pat_' prefix followed by 36 to 244 more characters (fine-grained PATs)", - "i" = "Read more at {.url {url}}." + "Invalid token validation mode: {.val {mode}}.", + "i" = "Must be one of {.val off}, {.val warn}, or {.val error}.", + "i" = "Configure via {.code options(gh_validate_tokens = ...)} or + env var {.envvar GH_VALIDATE_TOKENS}." )) } + mode } gh_pat <- function(x) { diff --git a/README.Rmd b/README.Rmd index 01dfbd5..31fffba 100644 --- a/README.Rmd +++ b/README.Rmd @@ -124,13 +124,18 @@ my_repos2 <- gh("GET /orgs/{org}/repos", org = "r-lib", page = 2) vapply(my_repos2, "[[", "", "name") ``` -## Environment Variables +## Environment Variables and Options * The `GITHUB_API_URL` environment variable is used for the default github api url. * The `GITHUB_PAT` and `GITHUB_TOKEN` environment variables are used, if set, in this order, as default token. Consider using the git credential store instead, see `?gh::gh_token`. +* The `GH_VALIDATE_TOKENS` environment variable controls what happens when + gh retrieves a PAT in an unrecognized format. Set it to `"off"` to skip + validation, `"warn"` (the default) to issue a warning and use the PAT + anyway, or `"error"` to abort. The `gh_validate_tokens` R option takes + precedence over the environment variable. ## Code of Conduct diff --git a/README.md b/README.md index c5639a0..642230b 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ gitcreds package. ### API URL -- The `GITHUB_API_URL` environment variable, if set, is used for the - default github api url. +- The `GITHUB_API_URL` environment variable, if set, is used for the + default github api url. ## Usage @@ -138,13 +138,18 @@ vapply(my_repos2, "[[", "", "name") #> [26] "liteq" "keyring" "sloop" "styler" "ansistrings" ``` -## Environment Variables - -- The `GITHUB_API_URL` environment variable is used for the default - github api url. -- The `GITHUB_PAT` and `GITHUB_TOKEN` environment variables are used, - if set, in this order, as default token. Consider using the git - credential store instead, see `?gh::gh_token`. +## Environment Variables and Options + +- The `GITHUB_API_URL` environment variable is used for the default + github api url. +- The `GITHUB_PAT` and `GITHUB_TOKEN` environment variables are used, if + set, in this order, as default token. Consider using the git + credential store instead, see `?gh::gh_token`. +- The `GH_VALIDATE_TOKENS` environment variable controls what happens + when gh retrieves a PAT in an unrecognized format. Set it to `"off"` + to skip validation, `"warn"` (the default) to issue a warning and use + the PAT anyway, or `"error"` to abort. The `gh_validate_tokens` R + option takes precedence over the environment variable. ## Code of Conduct diff --git a/man/gh_token.Rd b/man/gh_token.Rd index 401b7ac..fc52a0d 100644 --- a/man/gh_token.Rd +++ b/man/gh_token.Rd @@ -43,6 +43,17 @@ pre-selection of recommended scopes. Once you have a PAT, you can use point on, gh (via \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_get()}}) should be able to find it without further effort on your part. } +\section{Token format validation}{ +gh warns if the PAT it retrieves does not match a known format. +Set \code{options(gh_validate_tokens = "off")} or the +\code{GH_VALIDATE_TOKENS=off} environment variable to avoid this warning. +The option takes precedence over the environment variable. + +Set \code{options(gh_validate_tokens = "error")} or the +\code{GH_VALIDATE_TOKENS=error} environment variable to make gh throw an +error for an unrecognized PAT format. +} + \examples{ \dontrun{ gh_token() diff --git a/tests/testthat/_snaps/gh_whoami.md b/tests/testthat/_snaps/gh_whoami.md index 8ef5d35..f643f55 100644 --- a/tests/testthat/_snaps/gh_whoami.md +++ b/tests/testthat/_snaps/gh_whoami.md @@ -12,7 +12,7 @@ Condition Error in `gh()`: ! GitHub API error (401): Requires authentication - i Read more at + i Read more at Code gh_whoami(.token = "blah") Condition diff --git a/tests/testthat/test-gh_token.R b/tests/testthat/test-gh_token.R index 877984d..37a5d68 100644 --- a/tests/testthat/test-gh_token.R +++ b/tests/testthat/test-gh_token.R @@ -56,6 +56,7 @@ test_that("fall back to GITHUB_PAT, then GITHUB_TOKEN", { }) test_that("gh_token_exists works as expected", { + withr::local_options(gh_validate_tokens = "error") withr::local_envvar(GITHUB_API_URL = "https://test.com") withr::local_envvar(GITHUB_PAT_TEST_COM = NA) @@ -70,6 +71,8 @@ test_that("gh_token_exists works as expected", { # gh_pat class ---- test_that("validate_gh_pat() rejects bad characters, wrong # of characters", { + withr::local_options(gh_validate_tokens = "error") + # older PATs expect_error(gh_pat(strrep("a", 40)), NA) expect_error( @@ -96,6 +99,42 @@ test_that("validate_gh_pat() rejects bad characters, wrong # of characters", { ) }) +test_that("validate_gh_pat() honors gh_validate_tokens option and env var", { + bad <- "definitely-not-a-pat" + + # Default is "warn": warns but still returns the value. Reset the + # session-wide warning throttle so the expected warning isn't suppressed + # by an earlier firing. + withr::local_options(gh_validate_tokens = NULL) + withr::local_envvar(GH_VALIDATE_TOKENS = NA) + rlang::reset_warning_verbosity("gh_invalid_pat") + expect_warning(out <- gh_pat(bad), "Invalid GitHub PAT format") + expect_s3_class(out, "gh_pat") + expect_equal(unclass(out), bad) + + # "off" skips validation, no message of any kind. + withr::local_options(gh_validate_tokens = "off") + expect_silent(out <- gh_pat(bad)) + expect_equal(unclass(out), bad) + + # "error" aborts (current pre-default behavior). + withr::local_options(gh_validate_tokens = "error") + expect_error(gh_pat(bad), "Invalid GitHub PAT format") + + # Env var is honored when option is unset. + withr::local_options(gh_validate_tokens = NULL) + withr::local_envvar(GH_VALIDATE_TOKENS = "error") + expect_error(gh_pat(bad), "Invalid GitHub PAT format") + + # Option takes precedence over env var (env var still "error" from above). + withr::local_options(gh_validate_tokens = "off") + expect_silent(gh_pat(bad)) + + # Unknown mode errors loudly. + withr::local_options(gh_validate_tokens = "bogus") + expect_error(gh_pat(bad), "Invalid token validation mode") +}) + test_that("format.gh_pat() and str.gh_pat() hide the middle stuff", { pat <- paste0(strrep("a", 10), strrep("4", 20), strrep("F", 10)) expect_match(format(gh_pat(pat)), "[a-zA-Z]+")