Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Both actions authenticate once via `scripts/login.sh`, which logs the `posit` CL
1. Install `uv` and the `posit` CLI
2. Resolve config (server, GUID, entrypoint, extra_files) via `connect_actions.cli resolve-config`
3. Log in to Connect (`scripts/login.sh`)
4. Determine app type: if a `manifest.json` is present use `manifest`; otherwise query `app_mode` from the Connect content record (`posit connect api`) and run `connect_actions.cli resolve-app-type`, which maps it to a `posit connect deploy` subcommand (shiny, fastapi, flask, dash, streamlit, bokeh, quarto) and sets `needs_quarto` (true only when the resolved subcommand is `quarto`)
4. Determine app type: if a `manifest.json` is present use `manifest`; otherwise query `app_mode` from the Connect content record (`posit connect api`) and run `connect_actions.cli resolve-app-type`, which maps it to a `posit connect deploy` subcommand (shiny, fastapi, flask, dash, streamlit, bokeh, quarto) and sets `needs_quarto` (true only when the resolved subcommand is `quarto`). R app modes (`shiny`, `rmd-shiny`, `rmd-static`, `api`) require a `manifest.json` and will error clearly if it is not present.
5. Set up Quarto (`quarto-dev/quarto-actions/setup`) only when `needs_quarto` is true — the `quarto` subcommand runs `quarto inspect` locally to build the manifest
6. Check Connect capabilities: read the server version (`posit connect api server_settings -q .version`) and run `connect_actions.cli check-deploy-features`, which fails fast if a draft is requested on a server older than 2025.07.0 and sets the `send_metadata` output (false on servers older than 2025.12.0, or when the version can't be read)
7. Generate `requirements.txt` from `pyproject.toml` if missing (`generate-requirements.sh`)
Expand Down
32 changes: 29 additions & 3 deletions src/connect_actions/apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@
"quarto-static": "quarto",
}

# Connect ``app_mode`` values for R content. The ``posit`` CLI (via
# rsconnect-python) can only build a bundle for Python and Quarto content from
# source; R content has no source-deploy path here and must be deployed from a
# pre-built ``manifest.json`` (generated in R with ``rsconnect::writeManifest()``).
# We single these out so a missing manifest produces an R-tailored error instead
# of falling through to the Python requirements-generation path. ``quarto-shiny``
# is R-backed too but is handled separately (it falls through unchanged); see the
# note on ``APP_MODE_TO_TYPE`` above.
R_APP_MODES: frozenset[str] = frozenset(
{
"shiny", # R Shiny
"rmd-shiny", # interactive R Markdown
"rmd-static", # rendered R Markdown
"api", # R Plumber API
}
)


class AppTypeError(Exception):
"""Raised when the deploy subcommand can't be determined.
Expand All @@ -61,9 +78,11 @@ def resolve_app_type(*, manifest_present: bool, app_mode: str) -> AppType:

With a ``manifest.json`` present the type is ``manifest`` and Quarto is never
needed. Otherwise ``app_mode`` (from the Connect content record) is mapped to
a subcommand; an empty ``app_mode`` raises :class:`AppTypeError`. Only the
``quarto`` subcommand runs ``quarto inspect`` locally, so ``needs_quarto`` is
true exactly when the resolved type is ``quarto``.
a subcommand; an empty ``app_mode`` raises :class:`AppTypeError`, as does an R
``app_mode`` (R content has no source-deploy path here and needs a
``manifest.json``). Only the ``quarto`` subcommand runs ``quarto inspect``
locally, so ``needs_quarto`` is true exactly when the resolved type is
``quarto``.
"""
if manifest_present:
return AppType(deploy_type="manifest", needs_quarto=False)
Expand All @@ -74,5 +93,12 @@ def resolve_app_type(*, manifest_present: bool, app_mode: str) -> AppType:
"Provide a manifest.json or ensure the content GUID is correct."
)

if app_mode in R_APP_MODES:
raise AppTypeError(
f"R content (app_mode '{app_mode}') requires "
"a manifest.json. Generate one in R with rsconnect::writeManifest() and "
"commit it to your repository, then re-run the deploy."
)

deploy_type = APP_MODE_TO_TYPE.get(app_mode, app_mode)
return AppType(deploy_type=deploy_type, needs_quarto=deploy_type == "quarto")
23 changes: 23 additions & 0 deletions tests/test_apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,26 @@ def test_unknown_app_mode_falls_through_unchanged():
def test_empty_app_mode_without_manifest_errors():
with pytest.raises(AppTypeError, match="Could not determine app_mode"):
resolve_app_type(manifest_present=False, app_mode="")


@pytest.mark.parametrize("app_mode", ["shiny", "rmd-shiny", "rmd-static", "api"])
def test_r_app_modes_without_manifest_error_with_r_guidance(app_mode):
# R content can't be built from source here; the error must name R and point
# to a manifest.json rather than mentioning uv.lock/pyproject.toml/Python.
with pytest.raises(AppTypeError) as excinfo:
resolve_app_type(manifest_present=False, app_mode=app_mode)

message = str(excinfo.value)
assert "R" in message
assert "manifest.json" in message
assert "writeManifest" in message
assert "uv" not in message.lower()
assert "pyproject" not in message.lower()


def test_r_app_mode_with_manifest_still_deploys():
# A manifest.json is exactly the supported path for R content, so its presence
# short-circuits the R check and deploys the manifest directly.
result = resolve_app_type(manifest_present=True, app_mode="shiny")

assert result == AppType(deploy_type="manifest", needs_quarto=False)
Loading