feat: add content migrate-to-connect-cloud - #838
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.
Point a directory's local deployment record at an existing Posit Connect Cloud content item, so the next deploy from that directory updates that item instead of creating a second one. Connect Cloud cannot look content up by name, so the local record is the only way back to a content item; without one, deploying content that already exists there duplicates it. Nothing is copied and no bundle is uploaded. The record that was migrated from is removed, leaving the directory with one deployment target rather than two; `--from-server` chooses which record to migrate when the store covers several servers, and `-o/--overwrite` replaces an existing Connect Cloud record. Records are keyed by account, and a deploy refuses content owned by another account, so a record written under the wrong account would be silently useless. Migration therefore requires the content's owning account to be the one being targeted and names it when it is not. With no local record at all the record is reconstructed from the content, with app mode `unknown`, which `validate_app_mode` already tolerates. Fixes #826
|
| target rather than two; pass `--from-server` to choose which record to migrate | ||
| when there are several. | ||
|
|
||
| ## Unreleased |
| Only records for other servers are candidates: a Connect Cloud record is | ||
| what this migration produces, so treating one as a source would delete the | ||
| result. With no `from_server` a lone record is taken, and several are | ||
| reported rather than picked between. |
There was a problem hiding this comment.
Is this safe when the lone record is Connect's deployment record?
There was a problem hiding this comment.
i cleaned that up. now we preserve the Connect record and create a Connect Cloud record
…deploys Main merged the Connect Cloud deploy support as #840, a revised version of the e101baa work this branch carried. Every conflict is that same code in two forms, so all eight files take main's version wholesale; the Python 3.8 log authorization fix from 58e096c is already in #840. This drops `content migrate-to-connect-cloud`, which is unique to this branch. It is re-applied on top of main's code in the next commit.
Point a directory's local deployment record at an existing Posit Connect Cloud content item, so the next deploy from that directory updates that item instead of creating a second one. Connect Cloud cannot look content up by name, so the local record is the only way back to a content item; without one, deploying content that already exists there duplicates it. Nothing is copied and no bundle is uploaded. The record that was migrated from is removed, leaving the directory with one deployment target rather than two; `--from-server` chooses which record to migrate when the store covers several servers, and `-o/--overwrite` replaces an existing Connect Cloud record. Records are keyed by account, and a deploy refuses content owned by another account, so a record written under the wrong account would be silently useless. Migration therefore requires the content's owning account to be the one being targeted and names it when it is not. With no local record at all the record is reconstructed from the content, with app mode `unknown`, which `validate_app_mode` already tolerates. Widening `AppMetadata.app_guid` to `Optional[str]` also types the None that the Connect Cloud deploy in #840 already passes. Fixes #826
Two ways `content migrate-to-connect-cloud` could remove the source deployment record and leave behind one that cannot be deployed. A viewer role on the owning account passed the ownership check. `validate_connect_cloud_server` resolves publish permission through `get_account_by_name` only when the account id is not already known, so an id saved with a nickname skipped it and the refusal came from the deploy instead, after the source record was gone. The account is now checked before anything is written, reusing `_can_publish` so a viewer role is judged the same way in both places. The id-keyed and name-keyed record locations collide when an account's name equals its id, and removing the name-keyed record then deleted the record just written, emptying the store and reporting "The deployment record could not be saved." The fallback is now removed only when it differs from the target. Also drop the claim that migration leaves one deployment target: with `--from-server`, records for other servers are deliberately kept.
☂️ Python Coverage
Overall Coverage
New FilesNo new covered files... Modified Files
|
A lone deployment record for a Posit Connect server was silently taken as the migration source and removed, so the next deploy to that server created new content instead of updating the existing item — the duplication this command exists to prevent, on the Connect side. Removing the source is only justified for shinyapps.io: that content has been migrated away, so its record is dead. Connect content still exists and is still deployable, and deploying one directory to both Connect and Connect Cloud is a supported setup. The source record still supplies the title and app mode either way; only a shinyapps.io record is removed. `--from-server` chooses which record supplies the metadata; it does not make a live Connect record removable. Reported by vrsarah in review of #838.
karawoo
left a comment
There was a problem hiding this comment.
couple of small suggestions below but otherwise lgtm
| target = resolve_server_alias(from_server) | ||
| matches = [record for record in candidates if record.get("server_url") == target] |
There was a problem hiding this comment.
do we need to accommodate possible trailing slashes here?
There was a problem hiding this comment.
good catch! i will fix this
| ] | ||
|
|
||
| if from_server: | ||
| target = resolve_server_alias(from_server) |
There was a problem hiding this comment.
IIUC this will not work if someone tries to migrate a record from a Connect instance by passing --from-server <server name>, you'd have to pass the URL. But for migrating from shinyapps.io you can provide the server name. I know shinyapps.io is the main migration use case but it still would be nice to be consistent if it's not too hard.
There was a problem hiding this comment.
good call. i'll make that change
Deployment record keys are the server URL as typed at deploy time -- AbstractRemoteServer stores it verbatim -- so a record can be keyed "https://connect.example.com/" while --from-server names it without the slash, or the reverse. Either way migration_source_record's exact == found no match and the migration was refused. The rest of the function was already slash-insensitive: is_connect_cloud_url tolerates a trailing slash, and _is_shinyapps_record strips one. A shinyapps record stored with a slash was therefore classified correctly, and would have been removed by the migration, but could not be selected. Route all three comparisons through one _record_server_url helper instead of spelling the expression out per site. --from-server is stripped before resolve_server_alias, since the shinyapps.io short name resolves by exact comparison.
--from-server resolved only the two hard-coded pseudo server names (shinyapps.io, connect.posit.cloud) and never consulted the server store, so a Posit Connect instance had to be named by URL while shinyapps.io could be named by name. Look the value up as a nickname first, and fall back to the URL and alias handling. A nickname for a saved shinyapps.io server resolves to the same API URL as "shinyapps.io", so its record is removed like any other shinyapps.io source. A Connect record is still kept, as bd7cd7b established. Tested both ways. The option help called the value a URL while giving `shinyapps.io` as the example; it now names both forms. TestConnectCloudMigrate gets a temp-dir ServerStore, since resolving a nickname would otherwise read the developer's own saved servers.
Intent
Point a directory's local deployment record at an existing Posit Connect
Cloud content item, so the next deploy from that directory updates that
item instead of creating a second one. Connect Cloud cannot look content
up by name, so the local record is the only way back to a content item;
without one, deploying content that already exists there duplicates it.
Nothing is copied and no bundle is uploaded. The record that was
migrated from is removed, leaving the directory with one deployment
target rather than two;
--from-serverchooses which record to migratewhen the store covers several servers, and
-o/--overwritereplaces anexisting Connect Cloud record.
Records are keyed by account, and a deploy refuses content owned by
another account, so a record written under the wrong account would be
silently useless. Migration therefore requires the content's owning
account to be the one being targeted and names it when it is not. With
no local record at all the record is reconstructed from the content,
with app mode
unknown, whichvalidate_app_modealready tolerates.Fixes #826
Type of Change
Approach
This follows the patterns established by rstudio/rsconnect#1353 and rstudio/rsconnect#1358
Automated Tests
Unit tests exist, integration tests are scheduled but will happen after this merges.
Directions for Reviewers
These steps can be used to verify it works locally:
uv run rsconnect add -n shinyapps -A <shinyapps-account> -T <token> -S <secret>uv run rsconnect add -n cloud --connect-cloud -A <cc-account># opens a browseruv run rsconnect deploy shiny ./my-app -n shinyappsuv run rsconnect content migrate-to-connect-cloud ./my-app -n cloud --content-id <content-id>uv run rsconnect info ./my-appuv run rsconnect deploy shiny ./my-app -n cloudThis tool will also create a new deployment record if its missing. This test can be done without shinyapps.io or content migration in Connect Cloud.
uv run rsconnect deploy shiny ./my-app -n cloudrm -rf ./my-app/rsconnect-pythonChecklist
rsconnect-python-tests-at-nightworkflow in Connect against this feature branch.