feat: add support for deploying to Posit Connect Cloud - #837
Conversation
Adds Posit Connect Cloud as a deployment target alongside Posit Connect and shinyapps.io, mirroring the R rsconnect package's support: - Select the target with --connect-cloud or -s connect.posit.cloud. - Authenticate with an interactive OAuth device-code login or a service account client ID/secret (client credentials grant), with automatic token refresh and write-back to the credential store. - Register credentials with `rsconnect add`, verifying the account exists and grants the content:create permission before storing. - Deploy through the Connect Cloud revision model: create or update content, upload the bundle to a presigned URL, publish, poll the revision, and print the publish log from the logs service on failure. - Record deployments locally before publishing, since Connect Cloud cannot look content up by name. - Support the production, staging, and development environments via CONNECT_CLOUD_ENVIRONMENT, pinned to the saved server's URL. Fixes #817
# Conflicts: # docs/CHANGELOG.md
|
`cast(dict[str, Any], ...)` evaluates its first argument at runtime, so `from __future__ import annotations` does not cover it and `dict[...]` raises TypeError on Python 3.8. Describe the response with a TypedDict instead, matching the other ConnectCloudClient methods.
☂️ Python Coverage
Overall Coverage
New Files
Modified Files
|
There was a problem hiding this comment.
the changes in this file are added to support redaction of potential secrets from verbose/debug output. this isn't specific to the Connect Cloud support, but adding that support included more secrets (OAuth
tokens, client secrets, deploy-time secrets in bodies, presigned upload URLs whose query string is itself basically a credential) that I wanted to keep out of any logs.
The redaction. covers all deploy targets: Authorization headers, cookies, the bootstrap response's freshly minted API key, and shinyapps' X-Auth-Token/X-Auth-Signature. handle_bad_response redacts URIs quoted in error messages. output_params now also masks token, secret, client_secret, and -E env var values (previously only api_key)
| self.response_body = body | ||
| # None when the request failed before a response arrived (exception set), | ||
| # so status checks on a connection failure do not raise AttributeError. | ||
| self.status: int | None = None |
There was a problem hiding this comment.
Previously these attributes were only set when a response actually arrived, so code that checked response.status after a connection failure could hit AttributeError instead of a real error message. Downstream checks were updated for the Optional type: RSConnectClient.is_failed_response, the SPCS token-exchange handler (which now also surfaces response.exception properly), and a simplification in json_web_token.py (no need for hasattr)
| return wrapper | ||
|
|
||
|
|
||
| # Parameters whose values are credentials and must not reach the verbose log. |
There was a problem hiding this comment.
see comment at top of file on http_support.py
| "--cacert", | ||
| "-c", | ||
| envvar="CONNECT_CA_CERTIFICATE", | ||
| type=click.Path(exists=True, file_okay=True, dir_okay=False), |
There was a problem hiding this comment.
--cacert existence check moved from parse time to read time (and only when its relevant... ie, a Connect Cloud deploy doesn't care)
| # executor re-raises the all-or-nothing error after resolution when the | ||
| # target turns out not to be Connect Cloud. A token or secret is unambiguous | ||
| # shinyapps intent, so those still fail fast here. | ||
| lone_account_with_default = bool( |
There was a problem hiding this comment.
A lone -A/--account with a default server is no longer rejected immediately
| # Cloud content after the nickname. | ||
| target = resolve_pyproject_deploy_target( | ||
| pyproject_path, requirements_file=requirements_file, title_override=title or name | ||
| pyproject_path, requirements_file=requirements_file, title_override=title |
There was a problem hiding this comment.
Before, redeploying with -n mynickname silently renamed the content after the nickname. Now only an explicit -t/--title overrides the pyproject title. This applies to Connect deploys too.
| self.write_deployed_info() | ||
| return self | ||
|
|
||
| def record_server_key(self) -> str: |
There was a problem hiding this comment.
AppStore records were keyed by server URL; they're now keyed by record_server_key(), which is still exactly the URL for Connect/shinyapps/SPCS (no migration or behavior change) and url#account only for Connect Cloud, where every account shares one API URL. save_deployed_info was split so a write_deployed_info helper can record the content id early on Cloud; the Connect path is unchanged.
| return _login_device(server, cid, metadata, insecure, ca_data) | ||
| # Asking for the device flow against Connect usually means there is no | ||
| # usable browser, so do not try to open one. | ||
| return _login_device(server, cid, metadata, insecure, ca_data, open_browser=False) |
There was a problem hiding this comment.
the device-code flow gained an open_browser option. This passes False to preserve the existing Connect behavior.
| quarto: QuartoInspectResultQuarto | ||
| engines: list[str] | ||
| config: NotRequired[QuartoInspectResultConfig] | ||
| files: NotRequired[QuartoInspectResultFiles] |
There was a problem hiding this comment.
set in all quarto inspections, but only needed for the Connect Cloud path (to pick primary file)
| ), | ||
| no_args_is_help=True, | ||
| ) | ||
| @cli_exception_handler |
There was a problem hiding this comment.
Errors from add are now formatted like the deploy commands' instead of surfacing as raw tracebacks; this affects all add usage, not just Connect Cloud.
| def _get_present_options( | ||
| options: dict[str, Optional[Any]], | ||
| ctx: Optional[click.Context], | ||
| ignore_sources: tuple[str, ...] = (), |
There was a problem hiding this comment.
This is a shared validation change. _get_present_options can now skip option values that came from environment variables. Previously an env-sourced value (e.g. SHINYAPPS_ACCOUNT exported for a different CI job) was indistinguishable from a typed option and triggered "conflicting options" errors. Connect Cloud made this worse, a leftover CONNECT_API_KEY or CONNECT_SERVER shouldn't fail a --connect-cloud deploy, so the conflict checks now treat only typed options as intent and ignore irrelevant env values. Applied consistently across all targets, so nickname deploys with leftover shinyapps exports no longer error either. Existing passing invocations are unaffected.
| target_url = url | ||
| if use_connect_cloud and not explicit_cloud_url: | ||
| target_url = connect_cloud.SERVER_NAME | ||
| # A nickname names a saved credential, so environment-sourced shinyapps |
There was a problem hiding this comment.
| title override. The title now comes from `-t/--title` or the pyproject | ||
| metadata, so redeploying with a nickname no longer renames existing content | ||
| after the nickname. | ||
| - Added support for Python 3.14. The test suite now runs on Python 3.14 in CI. |
There was a problem hiding this comment.
i put my changes in the some Unreleased block as these pre-existing unreleased lines. Not sure if these were actually still unreleased or not.
|
@samperman A few behavioral/design questions and suggestions around target resolution and OAuth:
|
|
thanks for looking @mconflitti-pbc i think the answer to most of your questions would be "yes" if we only allowed a single you're right that a single credential can publish to multiple accounts, though, which given that:
|
|
changing this to a draft while i work on some refactoring |
Intent
Add Posit Connect Cloud as a deployment target, alongside Posit Connect and shinyapps.io, mirroring the R rsconnect package's support.
Fixes #817
Type of Change
Approach
--connect-cloud(or-s connect.posit.cloud) with-A <account>, or a saved nickname via-n. The production, staging, and development environments are selected withCONNECT_CLOUD_ENVIRONMENTand pinned to the saved server's URL.oauth.py,connect_cloud.py): interactive OAuth device-code login by default; service-account client ID/secret (client-credentials grant) via--client-id/--client-secretorCONNECT_CLOUD_CLIENT_ID/CONNECT_CLOUD_CLIENT_SECRETfor CI. Tokens refresh automatically on 401 and the rotated refresh token is written back to the credential store.rsconnect addverifies the account exists and grants thecontent:createpermission before storing.api.py): follows the Connect Cloud revision model — create or update content, upload the bundle to a presigned URL, publish, poll the revision, and print the publish log from the logs service on failure. Deployments are recorded locally before publishing because Connect Cloud cannot look content up by name.http_support.py): verbose logs and error messages now redact authorization/signing headers, OAuth tokens and client secrets in request/response bodies, presigned-URL query credentials, and-Eenvironment-variable values.Two behavior changes outside the Connect Cloud path (see CHANGELOG for details):
deploy manifest/deploy bundleno longer overwrite an existing content title when-tis not given, matching the other deploy commands.SHINYAPPS_*environment variables that happen to be set, anddeploy pyprojectno longer uses the nickname as a title override.Automated Tests
tests/test_connect_cloud.py(new, ~2800 lines): device-code and client-credentials auth flows, token refresh and write-back,rsconnect addverification, the full deploy/publish/poll flow, error paths and publish-log output, and environment selection — all HTTP mocked withhttpretty.tests/test_http_support.py: credential redaction coverage.test_metadata.py,test_oauth.py,test_validation.py, and others for the credential store and validation changes.Directions for Reviewers
rsconnect add -n cloud --connect-cloud -A <account>(opens a browser for device login; use--client-id/--client-secretfor a service account).rsconnect deploy shiny -n cloud ./app, then redeploy to exercise the update path.Checklist
rsconnect-python-tests-at-nightworkflow in Connect against this feature branch.