Skip to content

Commit 2b40759

Browse files
authored
chore(connect): clean up ordering in deploy-to-connect skill (#71)
2 parents 525b481 + 8cd344c commit 2b40759

1 file changed

Lines changed: 154 additions & 153 deletions

File tree

connect/deploy-to-connect/SKILL.md

Lines changed: 154 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: >-
99
or commands.
1010
metadata:
1111
author: posit-pbc
12-
version: "1.0"
12+
version: "2.0"
1313
---
1414

1515
# Deploying to Posit Connect
@@ -120,41 +120,15 @@ if you're on route 2.
120120
(`rsconnect deploy other-content` prints guidance for anything not in that
121121
list).
122122

123-
**The available frameworks and flags depend on the installed version** — that
124-
list is from **1.30.0**, and older versions have fewer (e.g. `bundle`, `git`,
125-
and `pyproject` are absent in 1.29.0). Always confirm against
126-
`rsconnect deploy --help` rather than trusting this list. If `uv tool run`
127-
resolves a stale cached version, pin it:
123+
**The available frameworks and flags depend on the installed version**.
124+
Always confirm against `rsconnect deploy --help` rather than trusting this list.
125+
If `uv tool run` resolves a stale cached version, pin it:
128126
`uv tool run --from 'rsconnect-python==1.30.0' rsconnect ...`.
129127

130128
### R content
131129

132-
Prefer the R `rsconnect` package targeting the Connect **server**. The flow is:
133-
register the server, register your API user, then deploy.
134-
135-
```r
136-
library(rsconnect)
137-
138-
# 1. Register the Connect server (once per server; name is a local nickname)
139-
rsconnect::addServer(url = "https://connect.example.com", name = "myserver")
140-
141-
# 2. Register your API user against that server (Connect SERVER auth)
142-
rsconnect::connectApiUser(
143-
server = "myserver",
144-
account = "your-username",
145-
apiKey = Sys.getenv("CONNECT_API_KEY")
146-
)
147-
148-
# 3. Deploy, choosing the function that matches the content:
149-
rsconnect::deployApp(appDir = ".", appTitle = "My App") # Shiny R, Plumber, dirs
150-
rsconnect::deployDoc("report.Rmd") # single Rmd / qmd
151-
rsconnect::deploySite(siteDir = ".") # Rmd/Quarto website
152-
```
153-
154-
> **Critical — this is Connect *server*, not Connect Cloud.** Use
155-
> `rsconnect::connectApiUser()` (or `connectUser()`), **never**
156-
> `connectCloudUser()`. The Cloud functions authenticate against a different
157-
> service and will not work here.
130+
Prefer the R `rsconnect` package targeting the Connect **server**, run through
131+
`Rscript -e '...'` or an R session.
158132

159133
Which deploy function to use:
160134

@@ -170,7 +144,7 @@ using a `manifest.json`.
170144
rsconnect deploy manifest ./manifest.json
171145
```
172146
- If there's no manifest and R *is* available elsewhere, generate one first with
173-
`rsconnect::writeManifest()` (see Stage 4).
147+
`rsconnect::writeManifest()` (see Stage 5).
174148
- If there's **neither R nor a manifest**, you cannot produce a valid R bundle.
175149
Surface this as a blocker: ask the user (if you have an ask-user tool) or
176150
report it clearly. Don't fake a deploy.
@@ -189,9 +163,45 @@ render. If the `.qmd` has R chunks and R is absent, treat it like R content
189163

190164
---
191165

192-
## Stage 4 — Resolve discrepancies (self-heal)
166+
## Stage 4 — Check credentials for that tool
167+
168+
Now that you know which tool you're using, check whether it can already reach the
169+
server. **This is a check, not a login** — being authenticated already is the
170+
common case (env vars set by CI, an account linked in an earlier session). If a
171+
path is live, do nothing here: don't run a login, don't run `rsconnect add`.
172+
173+
Both tools read `CONNECT_SERVER` / `CONNECT_API_KEY` from the environment, so
174+
start there either way:
175+
176+
```console
177+
env | grep -E '^CONNECT_(SERVER|API_KEY)=' | sed 's/=.*/=<set>/' # don't print the key
178+
```
193179

194-
When Stage 3 finds a gap, close it and **record the action**:
180+
**Python (rsconnect-python)** — saved servers and stored tokens:
181+
182+
```console
183+
rsconnect list
184+
```
185+
186+
**R (`rsconnect`)** — linked servers and accounts:
187+
188+
```console
189+
Rscript -e 'print(rsconnect::accounts())'
190+
```
191+
192+
If the tool itself isn't installed yet, the env-var check still tells you what
193+
you need; close the install gap in Stage 5, then run the tool-specific check.
194+
195+
Record the verdict: either a credential path is live (continue to Stage 6 once
196+
any other gaps are closed) or there is none, which is a discrepancy for Stage 5.
197+
Beware of having *two* paths live at once for Python — see the mixing warning in
198+
the [credentials reference](#credentials-reference).
199+
200+
---
201+
202+
## Stage 5 — Resolve discrepancies (self-heal)
203+
204+
When Stages 3 and 4 find a gap, close it and **record the action**:
195205

196206
- **`rsconnect` not on `PATH`** → don't install anything if `uv` is present;
197207
just run it on demand:
@@ -233,6 +243,11 @@ When Stage 3 finds a gap, close it and **record the action**:
233243
rsconnect write-manifest <framework> ./my-app
234244
```
235245
Then deploy the manifest via rsconnect-python if R can't deploy directly.
246+
- **No credentials** (Stage 4 found no env vars, no saved server, no linked
247+
account) → authenticate now, following
248+
[Credentials reference](#credentials-reference) for the ordering, the
249+
pitfalls, and what to do when nothing can supply them. Prefer a browser login
250+
(`rsconnect login`, `rsconnect::connectUser()`) when one is available.
236251
- **Dependencies** → you generally do **not** hand-list them. rsconnect and
237252
rsconnect-python scan the code and snapshot required package versions
238253
automatically (Python from `requirements.txt`/imports, R from your `.R`
@@ -244,83 +259,6 @@ When Stage 3 finds a gap, close it and **record the action**:
244259

245260
---
246261

247-
## Stage 5 — Authenticate
248-
249-
Deploying needs credentials for the Connect server.
250-
251-
### Python (rsconnect-python)
252-
253-
In order of preference:
254-
255-
1. **OAuth login (interactive).** Requires **rsconnect-python 1.30.0+** — check
256-
with `rsconnect version` first. One browser flow per server; tokens land in
257-
the OS keyring (falling back to a local credential store) and refresh
258-
automatically:
259-
```console
260-
rsconnect login https://connect.example.com
261-
rsconnect login https://connect.example.com --use-device-code # headless
262-
rsconnect logout https://connect.example.com # drop the tokens
263-
```
264-
In CI, skip the browser entirely by exchanging an OIDC identity token (e.g. a
265-
GitHub Actions token) for a short-lived Connect API key — prefer the
266-
`-file` form so the token never lands in process args or logs:
267-
```console
268-
rsconnect login https://connect.example.com --identity-token-file "$TOKEN_FILE"
269-
```
270-
2. **Env vars.** Best for headless/automated runs — no state to manage, and
271-
works on any version:
272-
```console
273-
export CONNECT_SERVER=https://connect.example.com
274-
export CONNECT_API_KEY=... # honored across the whole `rsconnect` surface
275-
```
276-
3. **Saved API-key nickname.** Save once, select later with `-n/--name`:
277-
```console
278-
rsconnect add -n myserver -s https://connect.example.com -k <api-key>
279-
rsconnect list # confirm what's saved
280-
```
281-
4. **Ad hoc flags** on the deploy command itself: `-s <url> -k <api-key>`.
282-
283-
**Shared credential flags:** `-n/--name` (saved server), `-s/--server` (env
284-
`CONNECT_SERVER`), `-k/--api-key` (env `CONNECT_API_KEY`), `-i/--insecure` (env
285-
`CONNECT_INSECURE`, for self-signed TLS), `-c/--cacert <file>` (env
286-
`CONNECT_CA_CERTIFICATE`).
287-
288-
> **Pick ONE auth path — never mix `-n` with env-var credentials.** rsconnect
289-
> rejects a command that combines a saved-server name (`-n/--name`) with
290-
> `CONNECT_SERVER`/`CONNECT_API_KEY` set in the environment, with an error like
291-
> `-n/--name (from COMMANDLINE) cannot be specified in conjunction with options
292-
> -s/--server (from ENVIRONMENT)`. Choose by what you have:
293-
>
294-
> - **`CONNECT_SERVER` and `CONNECT_API_KEY` are set** (typical headless/automated
295-
> run) → do **not** pass `-n`; let the env vars supply the target and key.
296-
> Deploy with just `rsconnect deploy <framework> <dir>`.
297-
> - **The request names a specific saved server** (e.g. "deploy to dogfood") →
298-
> use `-n dogfood`, and make sure `CONNECT_SERVER`/`CONNECT_API_KEY` are **not**
299-
> also exported for that command (`unset` them, or don't run `rsconnect add`
300-
> from a shell that has them set).
301-
>
302-
> If you have env-var creds but the request also names a server, prefer the env
303-
> vars (drop `-n`) — mixing is what triggers the rejection.
304-
305-
### R (`rsconnect`)
306-
307-
Register the server and API user as shown in Stage 3
308-
(`addServer()` + `connectApiUser()`), pulling the key from `CONNECT_API_KEY`.
309-
Check for already-linked accounts first:
310-
311-
```r
312-
rsconnect::accounts() # lists linked servers/accounts; empty => authenticate
313-
```
314-
315-
### If credentials are missing
316-
317-
- If you have an **ask-user / prompt tool**, ask the user for the server URL and
318-
API key.
319-
- Otherwise, rely on the `CONNECT_SERVER` / `CONNECT_API_KEY` env vars and, if
320-
they're absent, **report the missing credentials** rather than guessing.
321-
322-
---
323-
324262
## Stage 6 — Deploy and handle failure
325263

326264
### Discover the live command surface (Python)
@@ -336,41 +274,30 @@ rsconnect deploy <framework> --help # flags for one framework
336274

337275
### Deploy
338276

339-
```console
340-
rsconnect deploy streamlit ./my-app
341-
rsconnect deploy shiny ./my-shiny-app
342-
rsconnect deploy fastapi ./my-api
343-
rsconnect deploy quarto ./report
344-
rsconnect deploy manifest ./manifest.json # a prepared bundle
345-
```
346-
347-
Useful flags on any deploy command: `-t/--title`, `-N/--new` (force a new
348-
deployment instead of updating the recorded one), `-a/--app-id <id>` (target an
349-
existing item explicitly — mutually exclusive with `--new`), `-E NAME=VALUE`
350-
(set an environment variable, repeatable), `--draft` (keep serving the previous
351-
bundle until published).
352-
353-
For R, run the `deployApp()` / `deployDoc()` / `deploySite()` call from Stage 3.
277+
For Python, `rsconnect deploy <framework> <dir>` with the framework Stage 3
278+
picked (`manifest` takes the manifest file rather than a directory).
354279

355-
### Resolving `rsconnect` not found
280+
Non-obvious flags: `-t/--title`, `-N/--new` (force a new deployment instead of
281+
updating the recorded one), `-a/--app-id <id>` (target an existing item
282+
explicitly — mutually exclusive with `--new`), `-E NAME=VALUE` (set an
283+
environment variable, repeatable), `--draft` (keep serving the previous bundle
284+
until published).
356285

357-
rsconnect-python may be installed but not on `PATH` in the current shell (common
358-
in IDE-spawned terminals or when a virtualenv is active). Check
359-
`uv tool list | grep rsconnect`; either way, `uv tool run` works:
286+
For R, call the function Stage 3 selected, passing `appTitle` so the content
287+
isn't named after the directory.
360288

361-
```console
362-
uv tool run --from rsconnect-python rsconnect deploy shiny ./my-app -n myserver
363-
```
289+
### If `rsconnect` isn't found at deploy time
364290

365-
**Critical:** always pass `--from rsconnect-python` — the package name and the
366-
command name differ, so a bare `uv tool run rsconnect` won't resolve.
291+
It may be installed but off `PATH` in this shell (common in IDE-spawned
292+
terminals or with a virtualenv active). Fall back to `uv tool run` as described
293+
in Stage 5 — remembering `--from rsconnect-python`, since the package and
294+
command names differ.
367295

368296
### Pre-flight check (optional)
369297

370-
Before deploying, verify CLI access:
298+
To confirm the target is reachable and the credentials work before deploying:
371299

372300
```console
373-
rsconnect list # saved servers
374301
rsconnect details -n myserver # reachability + auth for one server
375302
```
376303

@@ -382,8 +309,9 @@ rsconnect details -n myserver # reachability + auth for one se
382309
`rsconnect login` (1.30.0+), or pass `-s`/`-k` (or set
383310
`CONNECT_SERVER`/`CONNECT_API_KEY`).
384311
- `-n/--name ... cannot be specified in conjunction with ... ENVIRONMENT`: you
385-
mixed a saved nickname with env-var credentials — see the Stage 5 warning.
386-
Drop `-n` or `unset CONNECT_SERVER CONNECT_API_KEY`.
312+
mixed a saved nickname with env-var credentials — see the mixing warning in
313+
the credentials reference. Drop `-n` or
314+
`unset CONNECT_SERVER CONNECT_API_KEY`.
387315
- `The requirements file 'requirements.txt' does not exist`: Python content
388316
needs one. Create it, point at another file with `--requirements-file`, or
389317
generate it with `--force-generate` (a `pip freeze`, so it may over-pin).
@@ -396,8 +324,10 @@ rsconnect details -n myserver # reachability + auth for one se
396324
**R:**
397325

398326
- "No account" / auth errors: run `rsconnect::accounts()`; if empty, re-run
399-
`rsconnect::addServer()` + `rsconnect::connectApiUser()`. Double-check you used
400-
`connectApiUser` (server), not `connectCloudUser` (Cloud).
327+
`rsconnect::addServer()` + `connectUser()`/`connectApiUser()`. Double-check you
328+
used a server function, not `connectCloudUser()` (Cloud).
329+
- Deploy hangs at an account prompt: more than one account is linked. Pass
330+
`account =` (and `server =`) explicitly to the deploy call.
401331
- Wrong deploy function: use `deployApp()` for directories/apps, `deployDoc()`
402332
for a single document, `deploySite()` for a site.
403333
- Self-signed TLS: pass the CA bundle via the `RETICULATE`/`curl` options or
@@ -408,15 +338,86 @@ rsconnect details -n myserver # reachability + auth for one se
408338

409339
---
410340

411-
## R `rsconnect` functions reference (Connect server)
341+
## Credentials reference
412342

413-
| Function | Purpose |
414-
| --- | --- |
415-
| `addServer(url, name)` | Registers a Connect **server** under a local nickname |
416-
| `connectApiUser(server, account, apiKey)` | Authenticates an API user against a Connect **server** (use this, **not** `connectCloudUser`) |
417-
| `accounts()` | Lists linked servers/accounts |
418-
| `deployApp(appDir, appTitle)` | Deploys a directory app — Shiny for R, Plumber, etc. |
419-
| `deployDoc(doc)` | Deploys a single document (Rmd, qmd) |
420-
| `deploySite(siteDir)` | Deploys a full R Markdown / Quarto site |
421-
| `writeManifest()` | Generates `manifest.json` (for the rsconnect-python / no-R route) |
422-
| `removeAccount(name)` | Removes a stored account from the local machine |
343+
How to authenticate when Stage 4 found no credentials and Stage 5 sent you here.
344+
If a credential path is already live, you don't need any of this.
345+
346+
### Python (rsconnect-python)
347+
348+
In order of preference:
349+
350+
1. **OAuth login (interactive).** Requires **rsconnect-python 1.30.0+** — check
351+
with `rsconnect version` first. One browser flow per server; tokens land in
352+
the OS keyring (falling back to a local credential store) and refresh
353+
automatically:
354+
```console
355+
rsconnect login https://connect.example.com
356+
rsconnect login https://connect.example.com --use-device-code # headless
357+
```
358+
2. **Saved API-key nickname.** Save once, select later with `-n/--name`:
359+
```console
360+
rsconnect add -n myserver -s https://connect.example.com -k <api-key>
361+
rsconnect list # confirm what's saved
362+
```
363+
3. **Env vars.** Best for headless/automated runs — no state to manage, and
364+
works on any version:
365+
```console
366+
export CONNECT_SERVER=https://connect.example.com
367+
export CONNECT_API_KEY=... # honored across the whole `rsconnect` surface
368+
```
369+
4. **Ad hoc flags** on the deploy command itself: `-s <url> -k <api-key>`.
370+
371+
**Shared credential flags:** `-n/--name` (saved server), `-s/--server` (env
372+
`CONNECT_SERVER`), `-k/--api-key` (env `CONNECT_API_KEY`), `-i/--insecure` (env
373+
`CONNECT_INSECURE`, for self-signed TLS), `-c/--cacert <file>` (env
374+
`CONNECT_CA_CERTIFICATE`).
375+
376+
> **Pick ONE auth path — never mix `-n` with env-var credentials.** rsconnect
377+
> rejects a command that combines a saved-server name (`-n/--name`) with
378+
> `CONNECT_SERVER`/`CONNECT_API_KEY` set in the environment, with an error like
379+
> `-n/--name (from COMMANDLINE) cannot be specified in conjunction with options
380+
> -s/--server (from ENVIRONMENT)`. Choose by what you have:
381+
>
382+
> - **`CONNECT_SERVER` and `CONNECT_API_KEY` are set** (typical headless/automated
383+
> run) → do **not** pass `-n`; let the env vars supply the target and key.
384+
> Deploy with just `rsconnect deploy <framework> <dir>`.
385+
> - **The request names a specific saved server** (e.g. "deploy to dogfood") →
386+
> use `-n dogfood`, and make sure `CONNECT_SERVER`/`CONNECT_API_KEY` are **not**
387+
> also exported for that command (`unset` them, or don't run `rsconnect add`
388+
> from a shell that has them set).
389+
>
390+
> If you have env-var creds but the request also names a server, prefer the env
391+
> vars (drop `-n`) — mixing is what triggers the rejection.
392+
393+
### R (`rsconnect`)
394+
395+
Register the server under a local nickname, then register your user against it:
396+
397+
```r
398+
library(rsconnect)
399+
400+
# 1. The server (once per server; the name is a local nickname)
401+
rsconnect::addServer(url = "https://connect.example.com", name = "myserver")
402+
403+
# 2a. Interactive — approve in a browser, no key to handle
404+
rsconnect::connectUser(server = "myserver")
405+
406+
# 2b. Or non-interactively (CI) — connectApiUser() requires an apiKey
407+
rsconnect::connectApiUser(
408+
server = "myserver",
409+
account = "your-username",
410+
apiKey = Sys.getenv("CONNECT_API_KEY")
411+
)
412+
```
413+
414+
> **Critical — this is Connect *server*, not Connect Cloud.** Use
415+
> `rsconnect::connectUser()` or `rsconnect::connectApiUser()`, **never**
416+
> `connectCloudUser()`. The Cloud functions authenticate against a different
417+
> service and will not work here.
418+
419+
### If no credentials can be found
420+
421+
- Rely on the `CONNECT_SERVER` / `CONNECT_API_KEY` env vars and, if
422+
they're absent, **report the missing credentials** rather than guessing
423+
or asking the user to paste an API key into the context.

0 commit comments

Comments
 (0)