diff --git a/NEWS.md b/NEWS.md index 22f619c3..b60df210 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,9 +4,13 @@ content was incorrectly published to a new location rather than reusing an existing deployment. (#981, #1007, #1013, #1019) -* Generated application names are always normalized and lower-cased when - deploying to shinyapps.io. The name is derived from an incoming title, when - provided, and otherwise from the content path. (#1022) +* When `deployApp()` is not given `appName`, the name is generated from an + incoming title, when provided. When the title is not provided, a title is + generated from the content path, which is then used to generate the + normalized application name. (#1022) + +* The application title recorded in the deployment record is updated with + server data. (#1008) * `showLogs()`, `configureApp()`, `setProperty()`, and `unsetProperty()` search for the application by name when there are no matching deployment @@ -40,7 +44,7 @@ * Ignore `.env`, `.venv`, and `venv` files only when they reference Python virtual environments. (#972) - + * `deployApp()` and `writeManifest()` accept optional `envManagement`, `envManagementR`, and `envManagementPy` arguments. These args specify whether Posit Connect should install packages in the package cache. @@ -105,29 +109,29 @@ * rsconnect requires renv 1.0.0. -* `deployApp()` and `writeManifest()` now respect renv lock files, if present. - If you don't want to use these lockfiles, and instead return the previous - behaviour of snapshotting on every deploy, add your `renv.lock` to +* `deployApp()` and `writeManifest()` now respect renv lock files, if present. + If you don't want to use these lockfiles, and instead return the previous + behaviour of snapshotting on every deploy, add your `renv.lock` to `.rscignore` (#671). Learn more `?appDependencies()`. - - Additionally, `deployApp()` and `writeManifest()` now use renv to capture app - dependencies, rather than packrat. If this causes a previously working deploy - to fail, please file an issue then set `options(rsconnect.packrat = TRUE)` to + + Additionally, `deployApp()` and `writeManifest()` now use renv to capture app + dependencies, rather than packrat. If this causes a previously working deploy + to fail, please file an issue then set `options(rsconnect.packrat = TRUE)` to revert to the previous behaviour. -* `deployApp()`'s `quarto` argument now takes values `TRUE`, `FALSE` or +* `deployApp()`'s `quarto` argument now takes values `TRUE`, `FALSE` or `NA`. The previous value (a path to a quarto binary) is now ignored, and instead we automatically figure out the package from `QUARTO_PATH` and `PATH` env vars (#658). `deploySite()` now supports quarto websites (#813). -* `deployApp()` gains a new `envVars` argument which takes a vector of the - names of environment variables that should be securely copied to the server. +* `deployApp()` gains a new `envVars` argument which takes a vector of the + names of environment variables that should be securely copied to the server. The names (not values) of these environment variables are also saved in the deployment record and will be updated each time you re-deploy the app (#667). - This currently only works with Connect, but we hope to add support to + This currently only works with Connect, but we hope to add support to Posit cloud and shinyapps.io in the future. - -* rsconnect gains two new functions for understanding and updating the + +* rsconnect gains two new functions for understanding and updating the environment variables that your apps currently use. `listServerEnvVars()` will return a data frame of applications, with a `envVars` list-column giving the names of the environment variables used by each application. @@ -137,7 +141,7 @@ ## Lifecycle changes -* Non-libcurl `rsconnect.http` options have been deprecated. This allows us to +* Non-libcurl `rsconnect.http` options have been deprecated. This allows us to focus our efforts on a single backend, rather than spreading development efforts across five. The old backends will remain available for at least 2 years, but if you are using them because libcurl doesn't work for you, please @@ -166,19 +170,19 @@ * Uploading large files to RPubs works once more (#450). * When recording details about deployments to Posit Cloud, appId now represents - the content id (as seen in URLs of the format + the content id (as seen in URLs of the format `https://posit.cloud/content/{id}`) instead of the application id. * Deployment records no longer contain the time the app was deployed (`when`) or when it's metadata was last synced (`lastSyncTime`) as these variables - are not very useful, and they lead to uninteresting diffs if you have - committed the deployment records to git (#770). A `version` field has been + are not very useful, and they lead to uninteresting diffs if you have + committed the deployment records to git (#770). A `version` field has been added to deployment DCF files to facilitate future file format changes, if needed. Its value for this release is `1`., * `accounts()` returns a zero-row data frame if no accounts are registered. -* `accountInfo()` and `removeAccount()` no longer require `account` be +* `accountInfo()` and `removeAccount()` no longer require `account` be supplied (#666). * `accountInfo()` and `servers()` redact sensitive information (secrets, @@ -187,8 +191,8 @@ * `addServer()` includes the port in the default server name, if present. -* `appDependencies()` includes implicit dependencies, and returns an additional - column giving the Repository (#670). Its documentation contains more +* `appDependencies()` includes implicit dependencies, and returns an additional + column giving the Repository (#670). Its documentation contains more information about how dependency discovery works, and how you can control it, if needed. @@ -197,13 +201,13 @@ * `connectApiUser()` now clearly requires an `apiKey` (#741). -* `deployApp()` output has been thoroughly reviewed and tweaked. As well as +* `deployApp()` output has been thoroughly reviewed and tweaked. As well as general polish it now gives you more information about what it has discovered about the deployment, like the app name, account & server, and which files are included in the bundle (#669). * `deployApp()` is more aggressive about saving deployment data, which should - make it less likely that you need to repeat yourself after a failed + make it less likely that you need to repeat yourself after a failed deployment. In particular, it now saves both before and after uploading the contents (#677) and it saves when you're updating content originally created by someone else (#270). @@ -212,70 +216,70 @@ visibility of an app deployed to posit.cloud (#838). * `deployApp()` now uses a stricter policy for determining whether or not - a locally installed package can be successfully installed on the deployment - server. This means that you're more likely to get a clean failure prior to + a locally installed package can be successfully installed on the deployment + server. This means that you're more likely to get a clean failure prior to deployment (#659). * `deployApp()` will now detect if you're attempting to publish to an app that has been deleted and will prompt you to create a new app (#226). * `deployApp()` includes some new conveniences for large uploads including - reporting the size of the bundle you're uploading and showing a progress bar + reporting the size of the bundle you're uploading and showing a progress bar in interactive sessions (#754). * `deployApp()` now follows redirects, which should make it more robust to your server moving to a new url (#674). * `deployApp()` uses simpler logic for determining whether it should create a - new app or update an existing app. Now `appName`, `account`, and `server` are - used to find existing deployments. If none are found, it will create a new - deployment; if one is found, it'll be updated; if more than one are found, it + new app or update an existing app. Now `appName`, `account`, and `server` are + used to find existing deployments. If none are found, it will create a new + deployment; if one is found, it'll be updated; if more than one are found, it will prompt you to disambiguate (#666). -* `deployApp()` improves account resolution from `account` and `server` - arguments by giving specific recommendations on the values that you might use - in the case of ambiguity or lack of matches (#666). Additionally, you'll now - receive a clear error if you accidentally provide something other than a +* `deployApp()` improves account resolution from `account` and `server` + arguments by giving specific recommendations on the values that you might use + in the case of ambiguity or lack of matches (#666). Additionally, you'll now + receive a clear error if you accidentally provide something other than a string or `NULL` to these arguments. * `deployApp()` now generates an interactive prompt to select `account`/`server` - (if no previous deployments) or `appName`/`account`/`server` (if multiple - previous deployments) (#691). + (if no previous deployments) or `appName`/`account`/`server` (if multiple + previous deployments) (#691). * `deployApp()` now advertises which startup scripts are run at the normal `logLevel`, and it evaluates each script in its own environment (#542). -* `deployApp()` now derives `appName` from `appDir` and `appPrimaryDoc`, - never using the title (#538). It now only simplifies the path if you are - publishing to shinyapps.io, since its restrictions on application names are +* `deployApp()` now derives `appName` from `appDir` and `appPrimaryDoc`, + never using the title (#538). It now only simplifies the path if you are + publishing to shinyapps.io, since its restrictions on application names are much tighter than those of Posit Connect. * `deployApp()` will now warn if `appFiles` or `appManifestFiles` contain files that don't exist, rather than silently ignoring them (#706). -* `deployApp()` excludes temporary backup files (names starting or ending - with `~`) when automatically determining files to bundle (#111) as well as - directories that are likely to be Python virtual environments (#632). - Additionally, ignore rules are always now applied to all directories; - previously some (like `.Rproj.user` and `"manifest.json"`) were only - applied to the root directory. It correctly handles `.rscignore` files - (i.e. as documented) (#568). +* `deployApp()` excludes temporary backup files (names starting or ending + with `~`) when automatically determining files to bundle (#111) as well as + directories that are likely to be Python virtual environments (#632). + Additionally, ignore rules are always now applied to all directories; + previously some (like `.Rproj.user` and `"manifest.json"`) were only + applied to the root directory. It correctly handles `.rscignore` files + (i.e. as documented) (#568). * `deployApp(appSourceDoc)` has been deprecated; it did the same job as `recordDir`. -* `deployDoc()` includes a `.Rprofile` in the bundle, if one is found in the +* `deployDoc()` includes a `.Rprofile` in the bundle, if one is found in the same directory as the document. * `lint()` should have fewer false positives for path problems: - the relative path linter has been removed (#244) and the case-sensitive + the relative path linter has been removed (#244) and the case-sensitive linter now only checks strings containing a `/` (#611). * New `listDeploymentFiles()`, which supsersedes `listBundleFiles()`. - It now errors when if the bundle is either too large or contains too many + It now errors when if the bundle is either too large or contains too many files, rather than silently truncating as before (#684). -* `serverInfo()` and `removeServer()` no longer require a `server` when +* `serverInfo()` and `removeServer()` no longer require a `server` when called interactively. * `showMetrics()` once again returns a correctly named data frame (#528). @@ -315,11 +319,11 @@ * Shiny applications and Shiny documents no longer include an implicit dependency on [`ragg`](https://ragg.r-lib.org) when that package is present in the local environment. This reverts a change introduced in 0.8.27. - + Shiny applications should add an explicit dependency on `ragg` (usually with a `library("ragg")` statement) to see it used by `shiny::renderPlot` (via `shiny::plotPNG`). - + The documentation for `shiny::plotPNG` explains the use of `ragg`. (#598) * Fix bug that prevented publishing or writing manifests for non-Quarto content @@ -341,8 +345,8 @@ an `.Rmd` file will successfully deploy as Quarto content instead of falling back to R Markdown. (#601) * If the `ragg` package is installed locally, it is now added as an implicit - dependency to `shiny` apps since `shiny::renderPlot()` now uses it by default - (when available). This way, `shiny` apps won't have to add `library(ragg)` to + dependency to `shiny` apps since `shiny::renderPlot()` now uses it by default + (when available). This way, `shiny` apps won't have to add `library(ragg)` to get consistent (higher-quality) PNG images when deployed. (#598) # rsconnect 0.8.26 diff --git a/R/deploymentTarget.R b/R/deploymentTarget.R index 0bc8f09f..8f4e4213 100644 --- a/R/deploymentTarget.R +++ b/R/deploymentTarget.R @@ -91,10 +91,12 @@ findDeploymentTarget <- function( # Otherwise, identify a target account (given just one available or prompted # by the user), generate a name, and locate the deployment. accountDetails <- findAccountInfo(account, server, error_call = error_call) - appName <- generateDefaultName( - recordPath = recordPath, - title = appTitle, - server = accountDetails$server + appTitle <- generateTitle(recordPath = recordPath, title = appTitle) + appName <- generateAppName( + appTitle = appTitle, + appPath = recordPath, + account = accountDetails$name, + unique = FALSE ) return(findDeploymentTargetByAppName( recordPath = recordPath, @@ -319,25 +321,7 @@ updateDeployment <- function(previous, appTitle = NULL, envVars = NULL) { ) } -normalizeName <- function(name, server = NULL) { - if (is.null(name)) { - return("") - } - - if (isShinyappsServer(server)) { - name <- tolower(name) - } - - # Replace non-alphanumerics with underscores, trim to length 64 - name <- gsub("[^[:alnum:]_-]+", "_", name, perl = TRUE) - name <- gsub("_+", "_", name) - if (nchar(name) > 64) { - name <- substr(name, 1, 64) - } - - name -} - +# Generate a title from a content path. titleFromPath <- function(recordPath) { if (isDocumentPath(recordPath)) { title <- file_path_sans_ext(basename(recordPath)) @@ -354,24 +338,13 @@ titleFromPath <- function(recordPath) { title } -# Determine name given a file or directory path and (optional) title. -# -# Prefer generating the name from the incoming title when provided and -# fall-back to one derived from the target filename. -# -# Name is guaranteed to conform to [a-zA-Z0-9_-]{0,64}. Minimum length is -# enforced by the server. -# -# Names produced for Shinyapps.io deployments are lower-cased. -generateDefaultName <- function(recordPath, title = NULL, server = NULL) { - name <- normalizeName(title, server = server) - - if (nchar(name) < 3) { - title <- titleFromPath(recordPath) - name <- normalizeName(title, server = server) +# Generate a title when a title was not already specified. +generateTitle <- function(recordPath, title = NULL) { + if (is.null(title)) { + titleFromPath(recordPath) + } else { + title } - - name } shouldUpdateApp <- function(application, diff --git a/R/deployments.R b/R/deployments.R index 076455bb..a4abd028 100644 --- a/R/deployments.R +++ b/R/deployments.R @@ -98,7 +98,7 @@ saveDeployment <- function(recordDir, addToHistory = TRUE) { deployment <- deploymentRecord( name = deployment$name, - title = deployment$title, + title = application$title %||% deployment$title, username = deployment$username, account = deployment$account, server = deployment$server, diff --git a/R/title.R b/R/title.R index 16d725b7..015a1455 100644 --- a/R/title.R +++ b/R/title.R @@ -57,18 +57,7 @@ generateAppName <- function(appTitle, appPath = NULL, account = NULL, unique = T # if we wound up with too few characters, try generating from the directory # name instead if (nchar(name) < 3 && !is.null(appPath) && file.exists(appPath)) { - # strip extension if present - base <- basename(appPath) - if (nzchar(tools::file_ext(base))) { - base <- file_path_sans_ext(base) - - # if we stripped an extension and the name is now "index", use the parent - # folder's name - if (identical(base, "index")) { - base <- basename(dirname(appPath)) - } - } - name <- munge(base) + name <- munge(titleFromPath(appPath)) } # validate that we wound up with a valid name diff --git a/tests/testthat/test-deploymentTarget.R b/tests/testthat/test-deploymentTarget.R index 7591ce29..8a8e8412 100644 --- a/tests/testthat/test-deploymentTarget.R +++ b/tests/testthat/test-deploymentTarget.R @@ -362,22 +362,19 @@ test_that("succeeds if there are no deployments and a single account", { expect_equal(deployment$server, "example.com") }) -test_that("default title is the empty string", { +test_that("default name and title derived from path", { local_temp_config() addTestServer() addTestAccount("ron") local_mocked_bindings( - getAppByName = function(...) data.frame( - name = "remotename", - url = "app-url", - stringsAsFactors = FALSE - ) + getAppByName = function(...) NULL ) - app_dir <- withr::local_tempdir() + app_dir <- dirCreate(file.path(withr::local_tempdir(), "MyApplication")) target <- findDeploymentTarget(app_dir, forceUpdate = TRUE) deployment <- target$deployment - expect_equal(deployment$title, "") + expect_equal(deployment$title, "MyApplication") + expect_equal(deployment$name, "myapplication") }) confirm_existing_app_used <- function(server) { @@ -385,7 +382,8 @@ confirm_existing_app_used <- function(server) { addTestServer() addTestAccount("ron", server = server) local_mocked_bindings(getAppByName = function(...) data.frame( - name = "my_app", + name = "remoteapp", + title = "Remote Application", id = 123, url = "http://example.com/test", stringsAsFactors = FALSE @@ -397,6 +395,8 @@ confirm_existing_app_used <- function(server) { target <- findDeploymentTarget(app_dir, appName = "my_app", server = server) deployment <- target$deployment expect_equal(deployment$appId, 123) + expect_equal(deployment$name, "remoteapp") + expect_equal(deployment$title, "Remote Application") } test_that("can find existing application on server & use it", { @@ -435,102 +435,43 @@ test_that("can find existing application on shinyapps.io & not use it", { confirm_existing_app_not_used("shinyapps.io") }) -# generateDefaultName --------------------------------------------------------- +# generateTitle --------------------------------------------------------- -test_that("generateDefaultName works with sites, documents, and directories", { +test_that("generateTitle works with sites, documents, and directories", { expect_equal( - generateDefaultName("foo/bar.Rmd", "This/is/a/TITLE"), - "This_is_a_TITLE" + generateTitle("foo/bar.Rmd", "This/is/a/TITLE"), + "This/is/a/TITLE" ) expect_equal( - generateDefaultName("foo/bar.Rmd", "NO"), - "bar" + generateTitle("foo/bar.Rmd", "NO"), + "NO" ) expect_equal( - generateDefaultName("foo/bar.Rmd"), + generateTitle("foo/bar.Rmd"), "bar" ) expect_equal( - generateDefaultName("foo/index.html"), + generateTitle("foo/index.html"), "foo" ) expect_equal( - generateDefaultName("foo/bar"), + generateTitle("foo/bar"), "bar" ) expect_equal( - generateDefaultName("foo/Awesome Document.Rmd"), - "Awesome_Document" + generateTitle("foo/Awesome Document.Rmd"), + "Awesome Document" ) expect_equal( - generateDefaultName("My Report/index.html"), - "My_Report" + generateTitle("My Report/index.html"), + "My Report" ) expect_equal( - generateDefaultName("foo/The-Application"), + generateTitle("foo/The-Application"), "The-Application" ) - - long_name <- strrep("AbCd", 64 / 4) - even_longer_name <- paste(long_name, "...") - expect_equal( - generateDefaultName(even_longer_name), - long_name - ) - expect_equal( - generateDefaultName("short-file-path", even_longer_name), - long_name - ) -}) - -test_that("generateDefaultName lower-cases names shinyApps", { - expect_equal( - generateDefaultName("foo/bar.Rmd", "This/is/a/TITLE", server = "shinyapps.io"), - "this_is_a_title" - ) - expect_equal( - generateDefaultName("foo/bar.Rmd", "NO", server = "shinyapps.io"), - "bar" - ) - - expect_equal( - generateDefaultName("foo/bar.Rmd", server = "shinyapps.io"), - "bar" - ) - expect_equal( - generateDefaultName("foo/index.html", server = "shinyapps.io"), - "foo" - ) - expect_equal( - generateDefaultName("foo/bar", server = "shinyapps.io"), - "bar" - ) - - expect_equal( - generateDefaultName("foo/Awesome Document.Rmd", server = "shinyapps.io"), - "awesome_document" - ) - expect_equal( - generateDefaultName("My Report/index.html", server = "shinyapps.io"), - "my_report" - ) - expect_equal( - generateDefaultName("foo/The-Application", server = "shinyapps.io"), - "the-application" - ) - - long_name <- strrep("AbCd", 64 / 4) - even_longer_name <- paste(long_name, "...") - expect_equal( - generateDefaultName(even_longer_name, server = "shinyapps.io"), - tolower(long_name) - ) - expect_equal( - generateDefaultName("short-file-path", even_longer_name, server = "shinyapps.io"), - tolower(long_name) - ) }) # helpers -----------------------------------------------------------------