mirror of
https://github.com/agdamsbo/FreesearchR.git
synced 2026-06-19 12:37:30 +02:00
325 lines
10 KiB
R
325 lines
10 KiB
R
|
|
# version_check.R
|
||
|
|
#
|
||
|
|
# Runs a one-time version check at app startup and returns a ready-made
|
||
|
|
# shinyWidgets::alert() UI element that can be placed directly in the UI
|
||
|
|
# definition -- no server(), no renderUI(), no uiOutput() required.
|
||
|
|
#
|
||
|
|
# Because the check runs outside server(), it executes once when the app
|
||
|
|
# process starts, so the banner is present immediately on first render with
|
||
|
|
# no loading delay.
|
||
|
|
#
|
||
|
|
# Version detection uses two strategies, tried in order:
|
||
|
|
# 1. utils::packageVersion() -- works when the package is installed locally.
|
||
|
|
# 2. app_version argument -- explicit fallback for environments where the
|
||
|
|
# package is not installed (e.g. shinyapps.io). Pass the result of your
|
||
|
|
# app_version() function here.
|
||
|
|
#
|
||
|
|
# Quick start:
|
||
|
|
#
|
||
|
|
# # global.R (or top of app.R, before ui / server)
|
||
|
|
# source("version_check.R")
|
||
|
|
# version_banner <- check_app_version(
|
||
|
|
# github_user = "your-github-username",
|
||
|
|
# github_repo = "your-repo-name",
|
||
|
|
# app_version = app_version() # fallback for shinyapps.io
|
||
|
|
# )
|
||
|
|
#
|
||
|
|
# # ui.R -- drop the result anywhere in the UI tree
|
||
|
|
# fluidPage(
|
||
|
|
# version_banner,
|
||
|
|
# ...
|
||
|
|
# )
|
||
|
|
#
|
||
|
|
# # Verbose / debug mode -- always show the banner:
|
||
|
|
# version_banner <- check_app_version(
|
||
|
|
# github_user = "your-github-username",
|
||
|
|
# github_repo = "your-repo-name",
|
||
|
|
# app_version = app_version(),
|
||
|
|
# verbose = TRUE
|
||
|
|
# )
|
||
|
|
|
||
|
|
|
||
|
|
# -- Internal helpers ----------------------------------------------------------
|
||
|
|
|
||
|
|
#' Check internet connectivity
|
||
|
|
#'
|
||
|
|
#' @return Logical; TRUE if an internet connection is available.
|
||
|
|
.has_internet <- function() {
|
||
|
|
tryCatch({
|
||
|
|
con <- url("https://api.github.com", open = "r")
|
||
|
|
close(con)
|
||
|
|
TRUE
|
||
|
|
}, error = function(e) FALSE)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
#' Fetch the latest release version from a GitHub repository
|
||
|
|
#'
|
||
|
|
#' @param github_user GitHub username or organisation.
|
||
|
|
#' @param github_repo Repository name.
|
||
|
|
#'
|
||
|
|
#' @return A character string with the version tag (e.g. "1.2.0"), or NULL on
|
||
|
|
#' failure.
|
||
|
|
.get_latest_github_version <- function(github_user, github_repo) {
|
||
|
|
api_url <- sprintf(
|
||
|
|
"https://api.github.com/repos/%s/%s/releases/latest",
|
||
|
|
github_user,
|
||
|
|
github_repo
|
||
|
|
)
|
||
|
|
|
||
|
|
tryCatch({
|
||
|
|
response <- readLines(url(api_url), warn = FALSE)
|
||
|
|
json_text <- paste(response, collapse = "")
|
||
|
|
|
||
|
|
tag <- regmatches(
|
||
|
|
json_text,
|
||
|
|
regexpr('"tag_name"\\s*:\\s*"([^"]+)"', json_text)
|
||
|
|
)
|
||
|
|
|
||
|
|
if (length(tag) == 0 || nchar(tag) == 0) return(NULL)
|
||
|
|
|
||
|
|
# Strip a leading "v" if present (e.g. "v1.2.0" -> "1.2.0")
|
||
|
|
sub('^"tag_name"\\s*:\\s*"v?([^"]+)"$', "\\1", tag)
|
||
|
|
}, error = function(e) NULL)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
#' Resolve the current app version
|
||
|
|
#'
|
||
|
|
#' Tries two strategies in order:
|
||
|
|
#' \enumerate{
|
||
|
|
#' \item \code{utils::packageVersion(package_name)} -- works when the package
|
||
|
|
#' is installed locally (development, local \code{runApp()}).
|
||
|
|
#' \item \code{app_version} argument -- an explicit version string supplied by
|
||
|
|
#' the caller, e.g. from an \code{app_version()} function bundled with the
|
||
|
|
#' app. Used on shinyapps.io where the package is not installed.
|
||
|
|
#' }
|
||
|
|
#'
|
||
|
|
#' @param package_name Name of the package / repository.
|
||
|
|
#' @param app_version Optional fallback version string.
|
||
|
|
#'
|
||
|
|
#' @return A character string with the version (e.g. "1.1.0"), or NULL if
|
||
|
|
#' neither strategy succeeds.
|
||
|
|
.resolve_app_version <- function(package_name, app_version = NULL) {
|
||
|
|
|
||
|
|
# Strategy 1: installed package
|
||
|
|
v <- tryCatch(
|
||
|
|
as.character(utils::packageVersion(package_name)),
|
||
|
|
error = function(e) NULL
|
||
|
|
)
|
||
|
|
if (!is.null(v)) {
|
||
|
|
message("[version_check] Version source: installed package")
|
||
|
|
return(v)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Strategy 2: explicit fallback supplied by the caller
|
||
|
|
if (!is.null(app_version)) {
|
||
|
|
message("[version_check] Version source: app_version() fallback")
|
||
|
|
return(as.character(app_version))
|
||
|
|
}
|
||
|
|
|
||
|
|
NULL
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
#' Build a shinyWidgets::alert() UI element for the version banner
|
||
|
|
#'
|
||
|
|
#' @param current Current installed version string.
|
||
|
|
#' @param latest Latest GitHub release version string, or NULL when
|
||
|
|
#' the check could not complete (e.g. no internet).
|
||
|
|
#' @param update_available Logical; whether latest > current.
|
||
|
|
#' @param github_user GitHub username / organisation.
|
||
|
|
#' @param github_repo Repository name.
|
||
|
|
#'
|
||
|
|
#' @return A \code{shinyWidgets::alert()} UI element.
|
||
|
|
.build_version_alert <- function(current,
|
||
|
|
latest,
|
||
|
|
update_available,
|
||
|
|
github_user,
|
||
|
|
github_repo) {
|
||
|
|
|
||
|
|
repo_url <- sprintf(
|
||
|
|
"https://github.com/%s/%s/releases/latest",
|
||
|
|
github_user,
|
||
|
|
github_repo
|
||
|
|
)
|
||
|
|
|
||
|
|
if (is.null(latest)) {
|
||
|
|
# Version check could not complete (no internet or API failure)
|
||
|
|
return(
|
||
|
|
shinyWidgets::alert(
|
||
|
|
tags$b("Version check failed. "),
|
||
|
|
sprintf(
|
||
|
|
"Running version %s. Could not reach GitHub to check for updates.",
|
||
|
|
current
|
||
|
|
),
|
||
|
|
status = "warning",
|
||
|
|
dismissible = TRUE
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (update_available) {
|
||
|
|
shinyWidgets::alert(
|
||
|
|
tags$b("Update available! "),
|
||
|
|
sprintf(
|
||
|
|
"You are running version %s. Version %s is available on GitHub.",
|
||
|
|
current, latest
|
||
|
|
),
|
||
|
|
" ",
|
||
|
|
tags$a(href = repo_url, target = "_blank", "View release"),
|
||
|
|
status = "warning",
|
||
|
|
dismissible = TRUE
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
# Up to date -- only shown in verbose mode
|
||
|
|
shinyWidgets::alert(
|
||
|
|
tags$b("Up to date. "),
|
||
|
|
sprintf(
|
||
|
|
"You are running version %s, which matches the latest release (%s).",
|
||
|
|
current, latest
|
||
|
|
),
|
||
|
|
status = "success",
|
||
|
|
dismissible = TRUE
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# -- Public API ----------------------------------------------------------------
|
||
|
|
|
||
|
|
#' Run a startup version check and return a banner UI element
|
||
|
|
#'
|
||
|
|
#' Call this \strong{outside} \code{server()} -- typically in
|
||
|
|
#' \code{global.R} or at the top of \code{app.R} -- and embed the returned
|
||
|
|
#' value directly in your UI definition. Because the check runs at startup
|
||
|
|
#' the banner is present on first render with no loading delay, and no
|
||
|
|
#' \code{uiOutput()} / \code{renderUI()} wiring is needed.
|
||
|
|
#'
|
||
|
|
#' \strong{Normal mode} (\code{verbose = FALSE}): returns a banner only when
|
||
|
|
#' a newer version is available or when the check fails. Returns \code{NULL}
|
||
|
|
#' when the app is up to date (Shiny silently ignores \code{NULL} in the UI).
|
||
|
|
#'
|
||
|
|
#' \strong{Verbose / debug mode} (\code{verbose = TRUE}): always returns a
|
||
|
|
#' banner -- including a success banner when up to date -- so you can confirm
|
||
|
|
#' the check ran and inspect both version strings during development.
|
||
|
|
#'
|
||
|
|
#' @param github_user GitHub username or organisation that owns the repository.
|
||
|
|
#' @param github_repo Repository name. Also used as the package name for
|
||
|
|
#' \code{utils::packageVersion()}.
|
||
|
|
#' @param app_version Optional fallback version string for environments where
|
||
|
|
#' the package is not installed (e.g. shinyapps.io). Pass the result of your
|
||
|
|
#' \code{app_version()} function here. Ignored when \code{packageVersion()}
|
||
|
|
#' succeeds.
|
||
|
|
#' @param verbose Logical; if \code{TRUE} a banner is always returned.
|
||
|
|
#' Defaults to \code{FALSE}.
|
||
|
|
#'
|
||
|
|
#' @return A \code{shinyWidgets::alert()} UI element, or \code{NULL} when there
|
||
|
|
#' is nothing to show (up to date in non-verbose mode).
|
||
|
|
#'
|
||
|
|
#' @examples
|
||
|
|
#' \dontrun{
|
||
|
|
#' # global.R or top of app.R
|
||
|
|
#' source("version_check.R")
|
||
|
|
#' version_banner <- check_app_version(
|
||
|
|
#' github_user = "my-org",
|
||
|
|
#' github_repo = "my-shiny-app",
|
||
|
|
#' app_version = app_version() # fallback for shinyapps.io
|
||
|
|
#' )
|
||
|
|
#'
|
||
|
|
#' # ui.R
|
||
|
|
#' fluidPage(
|
||
|
|
#' version_banner,
|
||
|
|
#' # ... rest of UI
|
||
|
|
#' )
|
||
|
|
#'
|
||
|
|
#' # Verbose mode for development
|
||
|
|
#' version_banner <- check_app_version(
|
||
|
|
#' github_user = "my-org",
|
||
|
|
#' github_repo = "my-shiny-app",
|
||
|
|
#' app_version = app_version(),
|
||
|
|
#' verbose = TRUE
|
||
|
|
#' )
|
||
|
|
#' }
|
||
|
|
check_app_version <- function(github_user,
|
||
|
|
github_repo,
|
||
|
|
app_version = NULL,
|
||
|
|
verbose = FALSE) {
|
||
|
|
|
||
|
|
# -- 1. Resolve current version ----------------------------------------------
|
||
|
|
local_version <- .resolve_app_version(github_repo, app_version)
|
||
|
|
if (is.null(local_version)) {
|
||
|
|
message(sprintf(
|
||
|
|
"[version_check] Could not determine version for '%s' (package not installed and no app_version() fallback supplied).",
|
||
|
|
github_repo
|
||
|
|
))
|
||
|
|
return(NULL)
|
||
|
|
}
|
||
|
|
|
||
|
|
message(sprintf("[version_check] Current version: %s", local_version))
|
||
|
|
|
||
|
|
# -- 2. Internet check -------------------------------------------------------
|
||
|
|
if (!.has_internet()) {
|
||
|
|
message("[version_check] No internet connection detected -- skipping.")
|
||
|
|
|
||
|
|
if (verbose) {
|
||
|
|
return(.build_version_alert(
|
||
|
|
current = local_version,
|
||
|
|
latest = NULL,
|
||
|
|
update_available = FALSE,
|
||
|
|
github_user = github_user,
|
||
|
|
github_repo = github_repo
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
return(NULL)
|
||
|
|
}
|
||
|
|
|
||
|
|
# -- 3. Fetch latest GitHub release ------------------------------------------
|
||
|
|
latest_version <- .get_latest_github_version(github_user, github_repo)
|
||
|
|
if (is.null(latest_version)) {
|
||
|
|
message("[version_check] Could not retrieve latest version from GitHub.")
|
||
|
|
|
||
|
|
if (verbose) {
|
||
|
|
return(.build_version_alert(
|
||
|
|
current = local_version,
|
||
|
|
latest = NULL,
|
||
|
|
update_available = FALSE,
|
||
|
|
github_user = github_user,
|
||
|
|
github_repo = github_repo
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
return(NULL)
|
||
|
|
}
|
||
|
|
|
||
|
|
message(sprintf("[version_check] Latest GitHub release: %s", latest_version))
|
||
|
|
|
||
|
|
# -- 4. Compare versions -----------------------------------------------------
|
||
|
|
update_available <- numeric_version(latest_version) > numeric_version(local_version)
|
||
|
|
|
||
|
|
if (update_available) {
|
||
|
|
message(sprintf(
|
||
|
|
"[version_check] Update available: %s -> %s",
|
||
|
|
local_version, latest_version
|
||
|
|
))
|
||
|
|
} else {
|
||
|
|
message(sprintf("[version_check] App is up to date (%s).", local_version))
|
||
|
|
}
|
||
|
|
|
||
|
|
# -- 5. Return banner --------------------------------------------------------
|
||
|
|
# An update was found -> always return a warning banner
|
||
|
|
# Up to date + verbose -> return a success banner
|
||
|
|
# Up to date + not verbose -> return NULL (Shiny ignores NULL in the UI)
|
||
|
|
if (update_available || verbose) {
|
||
|
|
.build_version_alert(
|
||
|
|
current = local_version,
|
||
|
|
latest = latest_version,
|
||
|
|
update_available = update_available,
|
||
|
|
github_user = github_user,
|
||
|
|
github_repo = github_repo
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
NULL
|
||
|
|
}
|
||
|
|
}
|