git clone --depth 1 https://github.com/posit-dev/skills /tmp/deploy-to-connect && cp -r /tmp/deploy-to-connect/connect/deploy-to-connect ~/.claude/skills/deploy-to-connectSKILL.md
<!--
Maintainer note: edit this skill in posit-dev/connect only.
Downstream copies are overwritten by the sync workflow.
-->
# Deploying to Posit Connect
This guide covers Python and R content on a Posit Connect server. Work through the stages in order.
Two toolchains do the work:
- Python — [rsconnect-python](https://github.com/posit-dev/rsconnect-python), which provides the `rsconnect` CLI and is published on PyPI.
- R — the R [`rsconnect`](https://rstudio.github.io/rsconnect/) package, pointed at a Connect server.
If the user asks a question ("how do I…", "what is the command…") rather than asking for a deploy, answer from this guide and stop.
At the end, report which server you deployed to, which content type you picked, any tool you installed, and any assumption you made.
---
## Stage 1 — Detect the content
Infer the language and framework from the files in the project directory. Common signals:
| Signal in project dir | Likely content |
| --- | --- |
| `app.py` | Python web app — Shiny for Python, Streamlit, Dash, Gradio, Panel, or Bokeh |
| `app.R`, or `ui.R` + `server.R` | Shiny for R |
| `plumber.R` / `entrypoint.R` containing `plumb()` | Plumber API (R) |
| `*.qmd` | Quarto document |
| `*.Rmd` | R Markdown |
| `*.ipynb` | Jupyter notebook / Voila |
| `manifest.json` | Prebuilt bundle — deploy it directly, no framework guess needed |
The imports in `app.py` name the framework:
```console
grep -Eo 'import (shiny|streamlit|dash|gradio|panel|bokeh)|from (shiny|streamlit|dash|gradio|panel|bokeh)' app.py
```
A bare ASGI or WSGI object means `fastapi` or `flask`.
Dependency files confirm the language: `requirements.txt` and `pyproject.toml` for Python, `DESCRIPTION` and `renv.lock` for R.
If the content is ambiguous (both Python and R files, or an `app.py` with no recognizable import), use your discretion, and report the assumption you made.
---
## Stage 2 — Inventory your tools
Probe the environment and build a capability set:
```console
command -v rsconnect # rsconnect-python on PATH
command -v uv # uv (installs and runs Python tools)
uv tool list 2>/dev/null | grep rsconnect # rsconnect-python installed via uv
command -v Rscript # R present
Rscript -e 'cat(requireNamespace("rsconnect", quietly=TRUE))' 2>/dev/null # R rsconnect package
command -v quarto # quarto CLI
command -v git # git
```
With `uv` present, Python content needs no install step. `uv tool run --from rsconnect-python rsconnect ...` fetches and runs the CLI on demand.
---
## Stage 3 — Pick a route
Cross the detected content (Stage 1) with your capabilities (Stage 2).
### Python content
Use rsconnect-python. With `rsconnect` on `PATH`:
```console
rsconnect deploy <framework> ./my-app
```
Off `PATH` but with `uv` present:
```console
uv tool run --from rsconnect-python rsconnect deploy <framework> ./my-app
```
Both forms take identical arguments. The rest of this guide writes the bare `rsconnect ...` form. Prefix it with `uv tool run --from rsconnect-python` when you use the second route.
`<framework>` is one of `api`, `bokeh`, `bundle`, `dash`, `fastapi`, `flask`, `git`, `gradio`, `html`, `manifest`, `nodejs`, `notebook`, `panel`, `pyproject`, `quarto`, `shiny`, `streamlit`, `tensorflow`, `voila`. For anything outside that list, `rsconnect deploy other-content` prints guidance.
The frameworks and flags depend on the installed version, so confirm against `rsconnect deploy --help` rather than this list. If `uv tool run` resolves a stale cached version, pin it: `uv tool run --from 'rsconnect-python==1.30.0' rsconnect ...`.
### R content
Use the R `rsconnect` package, through `Rscript -e '...'` or an R session:
- Shiny for R, Plumber API, or any app directory → `deployApp()`
- A single R Markdown or Quarto document → `deployDoc()`
- A full R Markdown or Quarto site → `deploySite()`
If `Rscript` is absent, deploy the R content through rsconnect-python with a `manifest.json`:
- A `manifest.json` already exists — deploy it directly:
```console
rsconnect deploy manifest ./manifest.json
```
- No manifest, but R is available elsewhere — generate one first with `rsconnect::writeManifest()` (see Stage 5).
- Neither R nor a manifest — a valid R bundle is not possible. Surface this as a blocker: ask the user or report it clearly.
### Quarto content
```console
rsconnect deploy quarto ./report
```
R-flavored Quarto (a `.qmd` with R code chunks) needs R to render. If R is absent, treat the document as R content and use the manifest route, or surface the gap.
---
## Stage 4 — Find the target and check its credentials
Now that the tool is known, find out which server to deploy to and whether the tool can already reach it. This is a check, not a login.
**Do not search the environment for API keys.** Do not read `CONNECT_API_KEY`, `CONNECT_SERVER`, a `.env` file, a keychain entry, or any other stored secret to pick a target or to register a server. Do this only when the user explicitly asks for it. An environment variable is not a request to use it.
List the accounts the tool already has. This is the only credential check you need.
```console
rsconnect list # Python: saved servers, stored tokens, and the default server on 1.30.0+
Rscript -e 'print(rsconnect::accounts())' # R: registered accounts
```
If the tool is not installed yet, close that gap in Stage 5 first. Then run the check.
Compare the result with the target the user named. Three outcomes:
- **An account matches the named target.** The credential path is live. Run no login and no `rsconnect add`. Continue to Stage 6 once the other gaps are closed.
- **The user named no target.** Ask them. List the servers the check found, and ask which one to deploy to, or whether they want a new target instead.>
Create and use brand.yml files for consistent branding across Shiny apps and Quarto documents. Covers: (1) Creating new _brand.yml files, (2) Applying to Shiny (R and Python), (3) Using in Quarto, (4) Modifying existing files, and (5) Troubleshooting. Includes complete specifications and integration guides.
Write ggsql queries — a grammar of graphics for SQL. Use when the user wants to create, modify, or understand a ggsql visualization query.
Creates a pull request from current changes, monitors GitHub CI, and debugs any failures until CI passes. Activate when the user says "create pr", "make a pr", "open pull request", "submit pr", "pr for these changes", or wants to get their current work into a reviewable PR. Assumes the project uses git, is hosted on GitHub, and has GitHub Actions CI with automated checks (lint, build, tests, etc.). Does NOT merge - stops when CI passes and provides the PR link.
Address PR review feedback by systematically working through every unresolved PR review thread on the current branch's PR - analyze each comment, make the requested code changes (with tests where useful), commit, and optionally reply and resolve.
Bulk resolve unresolved PR review threads on the current branch’s PR — typically after threads have been addressed manually or via /pr-threads-address
>
Guide for drafting issue closure and decline responses as an open-source package maintainer. Use when helping compose a reply that says \"no\" to a feature request, closes an issue as won't-fix, redirects a user to a different package, explains why a design choice is intentional, or otherwise declines or closes a community contribution. Also use when the maintainer needs to explain a deprecation, point out a user misunderstanding, or communicate an effort/scope tradeoff to a contributor.