diff --git a/NEWS.md b/NEWS.md
index ed56491ae..99a3ff221 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,12 @@
# connectapi (development version)
+## Breaking changes
+
+- `get_jobs()` now uses the public endpoint. The data returned by this function
+ has changed. In particular, the `finalized` column is no longer present,
+ replaced by `status`. `status == 0` corresponds to `isFALSE(finalized)`. See
+ `?get_jobs()` for more details about the new return format. (#340)
+
## New features
- `get_users()` now supports filtering users with the `account_status` and
diff --git a/R/connectapi.R b/R/connectapi.R
index 7c1e6d063..642cb3907 100644
--- a/R/connectapi.R
+++ b/R/connectapi.R
@@ -27,5 +27,6 @@ current_connect_version <- "2024.03.0"
.onLoad <- function(...) {
vctrs::s3_register("dplyr::collect", "tbl_connect")
+ vctrs::s3_register("vctrs::vec_cast", "character.integer")
invisible()
}
diff --git a/R/content.R b/R/content.R
index 890f25f70..d4e3eefd5 100644
--- a/R/content.R
+++ b/R/content.R
@@ -95,9 +95,23 @@ Content <- R6::R6Class(
},
#' @description Return the jobs for this content.
jobs = function() {
- warn_experimental("jobs")
- url <- unversioned_url("applications", self$get_content()$guid, "jobs")
- res <- self$get_connect()$GET(url)
+ res <- self$connect$GET(v1_url("content", self$content$guid, "jobs"), parser = NULL)
+ use_unversioned <- endpoint_does_not_exist(res)
+ if (use_unversioned) {
+ res <- self$connect$GET(unversioned_url("applications", self$content$guid, "jobs"), parser = NULL)
+ }
+ self$connect$raise_error(res)
+ parsed <- httr::content(res, as = "parsed")
+ if (use_unversioned) {
+ # The unversioned endpoint does not contain a `status` field. Its field
+ # `finalized` is `FALSE` corresponds to active jobs. The `finalized`
+ # field is dropped during parsing.
+ parsed <- purrr::modify_if(parsed, ~ isFALSE(.x$finalized), function(x) {
+ x$status <- 0
+ x
+ })
+ }
+ parsed
},
#' @description Return a single job for this content.
#' @param key The job key.
@@ -588,26 +602,73 @@ content_ensure <- function(
#' Get Jobs
#'
-#' `r lifecycle::badge('experimental')` Retrieve details about jobs associated with a `content_item`.
-#' "Jobs" in Posit Connect are content executions
+#' Retrieve details about server processes associated with a `content_item`,
+#' such as a FastAPI app or a Quarto render.
+#'
+#' Note that Connect versions below 2022.10.0 use a legacy endpoint, and will
+#' not return the complete set of information provided by newer versions.
#'
#' @param content A Content object, as returned by `content_item()`
-#' @param key The key for a job
#'
-#' @rdname jobs
+#' @return A data frame with a row for each job, with the following columns:
+#'
+#' - `id`: The job identifier.
+#' - `ppid`: The job's parent process identifier (see Note 1).
+#' - `pid`: The job's process identifier.
+#' - `key`: The job's unique key identifier.
+#' - `remote_id`: The job's identifier for off-host execution configurations
+#' (see Note 1).
+#' - `app_id`: The job's parent content identifier
+#' - `variant_id`: The identifier of the variant owning this job.
+#' - `bundle_id`: The identifier of a content bundle linked to this job.
+#' - `start_time`: The timestamp (RFC3339) indicating when this job started.
+#' - `end_time`: The timestamp (RFC3339) indicating when this job finished.
+#' - `last_heartbeat_time`: The timestamp (RFC3339) indicating the last time
+#' this job was observed to be running (see Note 1).
+#' - `queued_time`: The timestamp (RFC3339) indicating when this job was added
+#' to the queue to be processed. Only scheduled reports will present a value
+#' for this field (see Note 1).
+#' - `queue_name`: The name of the queue which processes the job. Only
+#' scheduled reports will present a value for this field (see Note 1).
+#' - `tag`: A tag to identify the nature of the job.
+#' - `exit_code`: The job's exit code. Present only when job is finished.
+#' - `status`: The current status of the job. On Connect 2022.10.0 and newer,
+#' one of Active: 0, Finished: 1, Finalized: 2; on earlier versions, Active:
+#' 0, otherwise `NA`.
+#' - `hostname`: The name of the node which processes the job.
+#' - `cluster`: The location where this content runs. Content running on the
+#' same server as Connect will have either a null value or the string Local.
+#' Gives the name of the cluster when run external to the Connect host
+#' (see Note 1).
+#' - `image`: The location where this content runs. Content running on
+#' the same server as Connect will have either a null value or the string
+#' Local. References the name of the target image when content runs in
+#' a clustered environment such as Kubernetes (see Note 1).
+#' - `run_as`: The UNIX user that executed this job.
+#'
+#' @note
+#' 1. On Connect instances earlier than 2022.10.0, these columns will contain `NA` values.
+#'
+#' @family job functions
#' @family content functions
#' @export
get_jobs <- function(content) {
- warn_experimental("get_jobs")
- scoped_experimental_silence()
validate_R6_class(content, "Content")
jobs <- content$jobs()
- parse_connectapi_typed(jobs, connectapi_ptypes$jobs)
+ parse_connectapi_typed(jobs, connectapi_ptypes$jobs, order_columns = TRUE)
}
# TODO: Need to test `logged_error` on a real error
-#' @rdname jobs
+#'
+#' Retrieve details about a server process
+#' associated with a `content_item`, such as a FastAPI app or a Quarto render.
+#'
+#' @param content A Content object, as returned by `content_item()`
+#' @param key The key for a job
+#'
+#' @family job functions
+#' @family content functions
#' @export
get_job <- function(content, key) {
warn_experimental("get_job")
diff --git a/R/parse.R b/R/parse.R
index 9b7af67ba..7152125db 100644
--- a/R/parse.R
+++ b/R/parse.R
@@ -27,7 +27,7 @@ make_timestamp <- function(input) {
safe_format(input, "%Y-%m-%dT%H:%M:%SZ", tz = "UTC", usetz = FALSE)
}
-ensure_columns <- function(.data, ptype) {
+ensure_columns <- function(.data, ptype, order_columns = FALSE) {
# Given a prototype, ensure that all columns are present and cast to the correct type.
# If a column is missing in .data, it will be created with all missing values of the correct type.
# If a column is present in both, it will be cast to the correct type.
@@ -35,6 +35,11 @@ ensure_columns <- function(.data, ptype) {
for (i in names(ptype)) {
.data <- ensure_column(.data, ptype[[i]], i)
}
+
+ if (order_columns) {
+ .data <- .data[, unique(c(names(ptype), names(.data))), drop = FALSE]
+ }
+
.data
}
@@ -59,14 +64,14 @@ ensure_column <- function(data, default, name) {
if (inherits(default, "list") && !inherits(col, "list")) {
col <- list(col)
}
- col <- vctrs::vec_cast(col, default)
+ col <- vctrs::vec_cast(col, default, x_arg = name)
}
data[[name]] <- col
data
}
-parse_connectapi_typed <- function(data, ptype) {
- ensure_columns(parse_connectapi(data), ptype)
+parse_connectapi_typed <- function(data, ptype, order_columns = FALSE) {
+ ensure_columns(parse_connectapi(data), ptype, order_columns)
}
parse_connectapi <- function(data) {
@@ -185,6 +190,10 @@ tzone <- function(x) {
attr(x, "tzone")[[1]] %||% ""
}
+vec_cast.character.integer <- function(x, to, ...) { # nolint: object_name_linter
+ as.character(x)
+}
+
new_datetime <- function(x = double(), tzone = "") {
tzone <- tzone %||% ""
if (is.integer(x)) {
diff --git a/R/ptype.R b/R/ptype.R
index 836cac1be..f8a255498 100644
--- a/R/ptype.R
+++ b/R/ptype.R
@@ -161,20 +161,26 @@ connectapi_ptypes <- list(
variant_key = NA_character_,
),
jobs = tibble::tibble(
- id = NA_integer_,
- pid = NA_integer_,
+ id = NA_character_,
+ ppid = NA_character_,
+ pid = NA_character_,
key = NA_character_,
- app_id = NA_integer_,
- app_guid = NA_character_,
- variant_id = NA_integer_,
- bundle_id = NA_integer_,
+ remote_id = NA_character_,
+ app_id = NA_character_,
+ variant_id = NA_character_,
+ bundle_id = NA_character_,
start_time = NA_datetime_,
end_time = NA_datetime_,
+ last_heartbeat_time = NA_datetime_,
+ queued_time = NA_datetime_,
+ queue_name = NA_character_,
tag = NA_character_,
exit_code = NA_integer_,
- finalized = NA,
+ status = NA_integer_,
hostname = NA_character_,
- variant_key = NA_character_
+ cluster = NA_character_,
+ image = NA_character_,
+ run_as = NA_character_,
),
job = tibble::tibble(
pid = NA_integer_,
diff --git a/R/utils.R b/R/utils.R
index 444b6243c..3a98fab28 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -186,3 +186,13 @@ token_hex <- function(n) {
raw <- as.raw(sample(0:255, n, replace = TRUE))
paste(as.character(raw), collapse = "")
}
+
+# Checks to see if an http status contains a 404 error, and that that 404
+# response does not contain an error code indicating that an endpoint was called
+# but encountered a 404 for some other reason.
+endpoint_does_not_exist <- function(res) {
+ return(
+ httr::status_code(res) == "404" &&
+ !("code" %in% names(httr::content(res, as = "parsed")))
+ )
+}
diff --git a/man/Content.Rd b/man/Content.Rd
index 6535367eb..4b2a04696 100644
--- a/man/Content.Rd
+++ b/man/Content.Rd
@@ -248,7 +248,7 @@ Return the URL for this content in the Posit Connect dashboard.
\if{html}{\out{}}
\if{latex}{\out{\hypertarget{method-Content-jobs}{}}}
\subsection{Method \code{jobs()}}{
-Return the jobs for this content.
+Return the jobs for this content
\subsection{Usage}{
\if{html}{\out{
}}\preformatted{Content$jobs()}\if{html}{\out{
}}
}
diff --git a/man/content_delete.Rd b/man/content_delete.Rd
index 6bf767e56..e2f412da5 100644
--- a/man/content_delete.Rd
+++ b/man/content_delete.Rd
@@ -32,6 +32,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/content_item.Rd b/man/content_item.Rd
index a0d90fcc6..2a5594c08 100644
--- a/man/content_item.Rd
+++ b/man/content_item.Rd
@@ -38,6 +38,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/content_title.Rd b/man/content_title.Rd
index 7b6b83f43..1ae5be076 100644
--- a/man/content_title.Rd
+++ b/man/content_title.Rd
@@ -34,6 +34,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/content_update.Rd b/man/content_update.Rd
index a0e271361..9bb62f685 100644
--- a/man/content_update.Rd
+++ b/man/content_update.Rd
@@ -54,6 +54,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/create_random_name.Rd b/man/create_random_name.Rd
index e00dc7998..0b02f0d6b 100644
--- a/man/create_random_name.Rd
+++ b/man/create_random_name.Rd
@@ -31,6 +31,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/dashboard_url.Rd b/man/dashboard_url.Rd
index e3dc08a8b..c224c3452 100644
--- a/man/dashboard_url.Rd
+++ b/man/dashboard_url.Rd
@@ -31,6 +31,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/dashboard_url_chr.Rd b/man/dashboard_url_chr.Rd
index 166a160c6..aa16dcb36 100644
--- a/man/dashboard_url_chr.Rd
+++ b/man/dashboard_url_chr.Rd
@@ -34,6 +34,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/delete_thumbnail.Rd b/man/delete_thumbnail.Rd
index 4cfbd83fe..2cb6fee1a 100644
--- a/man/delete_thumbnail.Rd
+++ b/man/delete_thumbnail.Rd
@@ -42,6 +42,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/delete_vanity_url.Rd b/man/delete_vanity_url.Rd
index 64dc15b35..14ba10a17 100644
--- a/man/delete_vanity_url.Rd
+++ b/man/delete_vanity_url.Rd
@@ -26,6 +26,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/deploy_repo.Rd b/man/deploy_repo.Rd
index 3a986c903..a0b610ecb 100644
--- a/man/deploy_repo.Rd
+++ b/man/deploy_repo.Rd
@@ -69,6 +69,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/environment.Rd b/man/environment.Rd
index 944b24555..5b5dc91c9 100644
--- a/man/environment.Rd
+++ b/man/environment.Rd
@@ -51,6 +51,7 @@ Other content functions:
\code{\link{deploy_repo}()},
\code{\link{get_bundles}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/get_bundles.Rd b/man/get_bundles.Rd
index e59af1143..5518b40dd 100644
--- a/man/get_bundles.Rd
+++ b/man/get_bundles.Rd
@@ -31,6 +31,7 @@ Other content functions:
\code{\link{deploy_repo}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
@@ -57,6 +58,7 @@ Other content functions:
\code{\link{deploy_repo}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/get_image.Rd b/man/get_image.Rd
index 154757802..b426c256a 100644
--- a/man/get_image.Rd
+++ b/man/get_image.Rd
@@ -41,6 +41,7 @@ Other content functions:
\code{\link{deploy_repo}()},
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/jobs.Rd b/man/get_job.Rd
similarity index 74%
rename from man/jobs.Rd
rename to man/get_job.Rd
index c3dcc17e5..044b5b042 100644
--- a/man/jobs.Rd
+++ b/man/get_job.Rd
@@ -1,12 +1,10 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/content.R
-\name{get_jobs}
-\alias{get_jobs}
+\name{get_job}
\alias{get_job}
-\title{Get Jobs}
+\title{Retrieve details about a server process
+associated with a \code{content_item}, such as a FastAPI app or a Quarto render.}
\usage{
-get_jobs(content)
-
get_job(content, key)
}
\arguments{
@@ -15,10 +13,13 @@ get_job(content, key)
\item{key}{The key for a job}
}
\description{
-\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} Retrieve details about jobs associated with a \code{content_item}.
-"Jobs" in Posit Connect are content executions
+Retrieve details about a server process
+associated with a \code{content_item}, such as a FastAPI app or a Quarto render.
}
\seealso{
+Other job functions:
+\code{\link{get_jobs}()}
+
Other content functions:
\code{\link{content_delete}()},
\code{\link{content_item}()},
@@ -33,6 +34,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
\code{\link{git}},
@@ -46,3 +48,4 @@ Other content functions:
\code{\link{verify_content_name}()}
}
\concept{content functions}
+\concept{job functions}
diff --git a/man/get_jobs.Rd b/man/get_jobs.Rd
new file mode 100644
index 000000000..d29cf1b1f
--- /dev/null
+++ b/man/get_jobs.Rd
@@ -0,0 +1,95 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/content.R
+\name{get_jobs}
+\alias{get_jobs}
+\title{Get Jobs}
+\usage{
+get_jobs(content)
+}
+\arguments{
+\item{content}{A Content object, as returned by \code{content_item()}}
+}
+\value{
+A data frame with a row for each job, with the following columns:
+\itemize{
+\item \code{id}: The job identifier.
+\item \code{ppid}: The job's parent process identifier (see Note 1).
+\item \code{pid}: The job's process identifier.
+\item \code{key}: The job's unique key identifier.
+\item \code{remote_id}: The job's identifier for off-host execution configurations
+(see Note 1).
+\item \code{app_id}: The job's parent content identifier
+\item \code{variant_id}: The identifier of the variant owning this job.
+\item \code{bundle_id}: The identifier of a content bundle linked to this job.
+\item \code{start_time}: The timestamp (RFC3339) indicating when this job started.
+\item \code{end_time}: The timestamp (RFC3339) indicating when this job finished.
+\item \code{last_heartbeat_time}: The timestamp (RFC3339) indicating the last time
+this job was observed to be running (see Note 1).
+\item \code{queued_time}: The timestamp (RFC3339) indicating when this job was added
+to the queue to be processed. Only scheduled reports will present a value
+for this field (see Note 1).
+\item \code{queue_name}: The name of the queue which processes the job. Only
+scheduled reports will present a value for this field (see Note 1).
+\item \code{tag}: A tag to identify the nature of the job.
+\item \code{exit_code}: The job's exit code. Present only when job is finished.
+\item \code{status}: The current status of the job. On Connect 2022.10.0 and newer,
+one of Active: 0, Finished: 1, Finalized: 2; on earlier versions, Active:
+0, otherwise \code{NA}.
+\item \code{hostname}: The name of the node which processes the job.
+\item \code{cluster}: The location where this content runs. Content running on the
+same server as Connect will have either a null value or the string Local.
+Gives the name of the cluster when run external to the Connect host
+(see Note 1).
+\item \code{image}: The location where this content runs. Content running on
+the same server as Connect will have either a null value or the string
+Local. References the name of the target image when content runs in
+a clustered environment such as Kubernetes (see Note 1).
+\item \code{run_as}: The UNIX user that executed this job.
+}
+}
+\description{
+Retrieve details about server processes associated with a \code{content_item},
+such as a FastAPI app or a Quarto render.
+}
+\details{
+Note that Connect versions below 2022.10.0 use a legacy endpoint, and will
+not return the complete set of information provided by newer versions.
+}
+\note{
+\enumerate{
+\item On Connect instances earlier than 2022.10.0, these columns will contain \code{NA} values.
+}
+}
+\seealso{
+Other job functions:
+\code{\link{get_job}()}
+
+Other content functions:
+\code{\link{content_delete}()},
+\code{\link{content_item}()},
+\code{\link{content_title}()},
+\code{\link{content_update}()},
+\code{\link{create_random_name}()},
+\code{\link{dashboard_url}()},
+\code{\link{dashboard_url_chr}()},
+\code{\link{delete_thumbnail}()},
+\code{\link{delete_vanity_url}()},
+\code{\link{deploy_repo}()},
+\code{\link{get_bundles}()},
+\code{\link{get_environment}()},
+\code{\link{get_image}()},
+\code{\link{get_job}()},
+\code{\link{get_thumbnail}()},
+\code{\link{get_vanity_url}()},
+\code{\link{git}},
+\code{\link{has_thumbnail}()},
+\code{\link{permissions}},
+\code{\link{set_image_path}()},
+\code{\link{set_run_as}()},
+\code{\link{set_thumbnail}()},
+\code{\link{set_vanity_url}()},
+\code{\link{swap_vanity_url}()},
+\code{\link{verify_content_name}()}
+}
+\concept{content functions}
+\concept{job functions}
diff --git a/man/get_thumbnail.Rd b/man/get_thumbnail.Rd
index 80ec48e3c..2bcdf8f9d 100644
--- a/man/get_thumbnail.Rd
+++ b/man/get_thumbnail.Rd
@@ -48,6 +48,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_vanity_url}()},
\code{\link{git}},
diff --git a/man/get_vanity_url.Rd b/man/get_vanity_url.Rd
index b90dcbf92..252bc7c09 100644
--- a/man/get_vanity_url.Rd
+++ b/man/get_vanity_url.Rd
@@ -30,6 +30,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{git}},
diff --git a/man/git.Rd b/man/git.Rd
index dbcf4b92c..0c8a03f7f 100644
--- a/man/git.Rd
+++ b/man/git.Rd
@@ -55,6 +55,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/has_thumbnail.Rd b/man/has_thumbnail.Rd
index 696569057..8fb4b96d9 100644
--- a/man/has_thumbnail.Rd
+++ b/man/has_thumbnail.Rd
@@ -44,6 +44,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/permissions.Rd b/man/permissions.Rd
index 05835eda3..519fc6540 100644
--- a/man/permissions.Rd
+++ b/man/permissions.Rd
@@ -77,6 +77,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/set_image.Rd b/man/set_image.Rd
index b5e644ba3..3007d1793 100644
--- a/man/set_image.Rd
+++ b/man/set_image.Rd
@@ -48,6 +48,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/set_run_as.Rd b/man/set_run_as.Rd
index 772594a1e..b1a26d707 100644
--- a/man/set_run_as.Rd
+++ b/man/set_run_as.Rd
@@ -50,6 +50,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/set_thumbnail.Rd b/man/set_thumbnail.Rd
index 0d3caf601..013003c16 100644
--- a/man/set_thumbnail.Rd
+++ b/man/set_thumbnail.Rd
@@ -47,6 +47,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/set_vanity_url.Rd b/man/set_vanity_url.Rd
index 854288056..f0ae9b097 100644
--- a/man/set_vanity_url.Rd
+++ b/man/set_vanity_url.Rd
@@ -43,6 +43,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/swap_vanity_url.Rd b/man/swap_vanity_url.Rd
index 36de9bf0d..98f087181 100644
--- a/man/swap_vanity_url.Rd
+++ b/man/swap_vanity_url.Rd
@@ -29,6 +29,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/man/verify_content_name.Rd b/man/verify_content_name.Rd
index 03abe180e..8610e9813 100644
--- a/man/verify_content_name.Rd
+++ b/man/verify_content_name.Rd
@@ -35,6 +35,7 @@ Other content functions:
\code{\link{get_bundles}()},
\code{\link{get_environment}()},
\code{\link{get_image}()},
+\code{\link{get_job}()},
\code{\link{get_jobs}()},
\code{\link{get_thumbnail}()},
\code{\link{get_vanity_url}()},
diff --git a/tests/testthat/2024.07.0/README.md b/tests/testthat/2024.07.0/README.md
new file mode 100644
index 000000000..185124ace
--- /dev/null
+++ b/tests/testthat/2024.07.0/README.md
@@ -0,0 +1 @@
+This dir for mocks that force the use of legacy, unversioned APIs.
\ No newline at end of file
diff --git a/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/job/QBZ5jkKfmf9iT9k8.json b/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/job/QBZ5jkKfmf9iT9k8.json
new file mode 100644
index 000000000..cf4fbdad5
--- /dev/null
+++ b/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/job/QBZ5jkKfmf9iT9k8.json
@@ -0,0 +1,18 @@
+{
+ "ppid": 2509183,
+ "pid": 2509199,
+ "key": "QBZ5jkKfmf9iT9k8",
+ "app_id": 52389,
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "tag": "run_app",
+ "finalized": true,
+ "hostname": "dogfood02",
+ "origin": "0001-01-01T00:00:00Z",
+ "stdout": "2024/11/25 22:17:43.386988748 Running on host: dogfood02\n2024/11/25 22:17:43.387008093 Process ID: 2509199\n2024/11/25 22:17:43.430980380 Linux distribution: Ubuntu 22.04.2 LTS (jammy)\n2024/11/25 22:17:43.441252960 Running as user: uid=1031(rstudio-connect) gid=999(rstudio-connect) groups=999(rstudio-connect)\n2024/11/25 22:17:43.441266159 Connect version: 2024.12.0-dev+41-g32b4bddee2\n2024/11/25 22:17:43.441426656 LANG: C.UTF-8\n2024/11/25 22:17:43.441433843 Working directory: /opt/rstudio-connect/mnt/app\n2024/11/25 22:17:43.442234301 Using R 4.3.1\n2024/11/25 22:17:43.442251215 R.home(): /opt/R/4.3.1/lib/R\n2024/11/25 22:17:43.447902588 Content will use associated Packrat library\n2024/11/25 22:17:43.582826876 Adding Packrat library to R_LIBS and .libPaths: /opt/rstudio-connect/mnt/app/packrat/lib/x86_64-pc-linux-gnu/4.3.1\n2024/11/25 22:17:43.583062744 R_LIBS: /opt/rstudio-connect/mnt/app/packrat/lib/x86_64-pc-linux-gnu/4.3.1\n2024/11/25 22:17:43.583081371 .libPaths(): /opt/rstudio-connect/mnt/app/packrat/lib/x86_64-pc-linux-gnu/4.3.1, /opt/R/4.3.1/lib/R/library\n2024/11/25 22:17:43.614560696 shiny version: 1.7.4\n2024/11/25 22:17:43.614602630 httpuv version: 1.6.11\n2024/11/25 22:17:43.614782708 rmarkdown version: (none)\n2024/11/25 22:17:43.614785084 knitr version: (none)\n2024/11/25 22:17:43.614794794 jsonlite version: 1.8.4\n2024/11/25 22:17:43.614795565 RJSONIO version: (none)\n2024/11/25 22:17:43.614802828 htmltools version: 0.5.5\n2024/11/25 22:17:43.614803648 reticulate version: (none)\n2024/11/25 22:17:43.615021102 Using pandoc: /opt/rstudio-connect/ext/pandoc/2.16\n2024/11/25 22:17:44.634166987 Using Shiny bookmarking base directory /opt/rstudio-connect/mnt/bookmarks\n2024/11/25 22:17:44.635399206 Shiny application starting ...\n",
+ "stderr": "2024/11/25 22:17:43.030795136 [rsc-session] Content GUID: 8f37d6e0\n2024/11/25 22:17:43.030842219 [rsc-session] Content ID: 52389\n2024/11/25 22:17:43.030849040 [rsc-session] Bundle ID: 127015\n2024/11/25 22:17:43.030854033 [rsc-session] Job Key: QBZ5jkKfmf9iT9k8\n2024/11/25 22:17:45.068540867 \n2024/11/25 22:17:45.068555326 Listening on http://127.0.0.1:33545\n",
+ "logged_error": null,
+ "exit_code": 0,
+ "start_time": 1732573062,
+ "end_time": null
+}
diff --git a/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/jobs.json b/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/jobs.json
new file mode 100644
index 000000000..d71beee2b
--- /dev/null
+++ b/tests/testthat/2024.07.0/__api__/applications/8f37d6e0/jobs.json
@@ -0,0 +1,77 @@
+[
+ {
+ "id": 40669829,
+ "pid": 1153321,
+ "key": "k3sHkEoWJNwQim7g",
+ "app_id": 52389,
+ "app_guid": "8f37d6e0",
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "start_time": 1733268003,
+ "end_time": null,
+ "tag": "run_app",
+ "exit_code": null,
+ "finalized": false,
+ "hostname": "dogfood01"
+ },
+ {
+ "id": 40097386,
+ "pid": 2516505,
+ "key": "mxPGVOMVk6f8dso2",
+ "app_id": 52389,
+ "app_guid": "8f37d6e0",
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "start_time": 1732573574,
+ "end_time": 1732577139,
+ "tag": "run_app",
+ "exit_code": 0,
+ "finalized": true,
+ "hostname": "dogfood02"
+ },
+ {
+ "id": 40096649,
+ "pid": 2509199,
+ "key": "QBZ5jkKfmf9iT9k8",
+ "app_id": 52389,
+ "app_guid": "8f37d6e0",
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "start_time": 1732573062,
+ "end_time": null,
+ "tag": "run_app",
+ "exit_code": null,
+ "finalized": true,
+ "hostname": "dogfood02"
+ },
+ {
+ "id": 40080413,
+ "pid": 2321354,
+ "key": "EzxM4sBYJrLSMHg9",
+ "app_id": 52389,
+ "app_guid": "8f37d6e0",
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "start_time": 1732553145,
+ "end_time": 1732556770,
+ "tag": "run_app",
+ "exit_code": 0,
+ "finalized": true,
+ "hostname": "dogfood02"
+ },
+ {
+ "id": 39368207,
+ "pid": 2434200,
+ "key": "HbdzgOJrMmMTq6vu",
+ "app_id": 52389,
+ "app_guid": "8f37d6e0",
+ "variant_id": 0,
+ "bundle_id": 127015,
+ "start_time": 1731690180,
+ "end_time": 1731690383,
+ "tag": "run_app",
+ "exit_code": 0,
+ "finalized": true,
+ "hostname": "dogfood02"
+ }
+]
diff --git a/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs.R b/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs.R
new file mode 100644
index 000000000..abece4989
--- /dev/null
+++ b/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs.R
@@ -0,0 +1,8 @@
+structure(
+ list(
+ url = "__api__/v1/content/8f37d6e0",
+ status_code = 404L, content = charToRaw("404 page not found"),
+ headers = structure(list(`content-type` = "text/plain"), class = "insensitive")
+ ),
+ class = "response"
+)
diff --git a/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.R b/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.R
new file mode 100644
index 000000000..762c94f22
--- /dev/null
+++ b/tests/testthat/2024.07.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.R
@@ -0,0 +1,8 @@
+structure(
+ list(
+ url = "__api__/v1/content/01234567/thumbnail",
+ status_code = 404L, content = charToRaw("404 page not found"),
+ headers = structure(list(`content-type` = "text/plain"), class = "insensitive")
+ ),
+ class = "response"
+)
diff --git a/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs.json b/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs.json
new file mode 100644
index 000000000..eb81c29b8
--- /dev/null
+++ b/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs.json
@@ -0,0 +1,112 @@
+[
+ {
+ "id": "40669829",
+ "ppid": "1153303",
+ "pid": "1153321",
+ "key": "k3sHkEoWJNwQim7g",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-12-03T23:20:03Z",
+ "end_time": null,
+ "last_heartbeat_time": "2024-12-03T23:20:13Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": null,
+ "status": 0,
+ "hostname": "dogfood01",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+ },
+ {
+ "id": "40097386",
+ "ppid": "2516489",
+ "pid": "2516505",
+ "key": "mxPGVOMVk6f8dso2",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-11-25T22:26:14Z",
+ "end_time": "2024-11-25T23:25:39Z",
+ "last_heartbeat_time": "2024-11-25T23:25:36Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": 0,
+ "status": 2,
+ "hostname": "dogfood02",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+ },
+ {
+ "id": "40096649",
+ "ppid": "2509183",
+ "pid": "2509199",
+ "key": "QBZ5jkKfmf9iT9k8",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-11-25T22:17:42Z",
+ "end_time": null,
+ "last_heartbeat_time": "2024-11-25T22:20:23Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": null,
+ "status": 2,
+ "hostname": "dogfood02",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+ },
+ {
+ "id": "40080413",
+ "ppid": "2321337",
+ "pid": "2321354",
+ "key": "EzxM4sBYJrLSMHg9",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-11-25T16:45:45Z",
+ "end_time": "2024-11-25T17:46:10Z",
+ "last_heartbeat_time": "2024-11-25T17:46:08Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": 0,
+ "status": 2,
+ "hostname": "dogfood02",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+ },
+ {
+ "id": "39368207",
+ "ppid": "2434183",
+ "pid": "2434200",
+ "key": "HbdzgOJrMmMTq6vu",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-11-15T17:03:00Z",
+ "end_time": "2024-11-15T17:06:23Z",
+ "last_heartbeat_time": "2024-11-15T17:06:20Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": 0,
+ "status": 2,
+ "hostname": "dogfood02",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+ }
+]
diff --git a/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.json b/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.json
new file mode 100644
index 000000000..b3d6bd65b
--- /dev/null
+++ b/tests/testthat/2024.08.0/__api__/v1/content/8f37d6e0/jobs/QBZ5jkKfmf9iT9k8.json
@@ -0,0 +1,22 @@
+{
+ "id": "40096649",
+ "ppid": "2509183",
+ "pid": "2509199",
+ "key": "QBZ5jkKfmf9iT9k8",
+ "remote_id": null,
+ "app_id": "52389",
+ "variant_id": "0",
+ "bundle_id": "127015",
+ "start_time": "2024-11-25T22:17:42Z",
+ "end_time": null,
+ "last_heartbeat_time": "2024-11-25T22:20:23Z",
+ "queued_time": null,
+ "queue_name": null,
+ "tag": "run_app",
+ "exit_code": null,
+ "status": 2,
+ "hostname": "dogfood02",
+ "cluster": null,
+ "image": null,
+ "run_as": "rstudio-connect"
+}
diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R
index e26d202bc..2dc90e074 100644
--- a/tests/testthat/test-content.R
+++ b/tests/testthat/test-content.R
@@ -241,3 +241,32 @@ with_mock_api({
expect_identical(v$key, "WrEKKa77")
})
})
+
+# jobs -----
+
+test_that("get_jobs() using the old and new endpoints returns sensible results", {
+ with_mock_api({
+ client <- Connect$new(server = "http://connect.example", api_key = "not-a-key")
+ item <- content_item(client, "8f37d6e0")
+ jobs_v1 <- get_jobs(item)
+ TRUE
+ })
+
+ with_mock_dir("2024.07.0", {
+ jobs_v0 <- get_jobs(item)
+ })
+
+ # Columns we expect to be identical
+ common_cols <- c(
+ "id", "pid", "key", "app_id", "variant_id", "bundle_id", "start_time",
+ "end_time", "tag", "exit_code", "hostname"
+ )
+ expect_equal(
+ jobs_v1[common_cols],
+ jobs_v1[common_cols]
+ )
+
+ # Status columns line up as expected
+ expect_equal(jobs_v1$status, c(0L, 2L, 2L, 2L, 2L))
+ expect_equal(jobs_v0$status, c(0L, NA, NA, NA, NA))
+})