diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab26432..a5d40807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and `*_app.py` are now considered. However, if the directory contains more than one file matching these new patterns, you must provide rsconnect-python with an explicit `--entrypoint` argument. +- Added support for deploying directly from remote git repositories. Only + Connect server targets are supported, and the Connect server must have git + configured with access to your git repositories. See the + [Connect administrator guide](https://docs.posit.co/connect/admin/content-management/git-backed/) + and + [Connect user guide](https://docs.posit.co/connect/user/git-backed/) for details. - Added a new verbose logging level. Specifying `-v` on the command line uses this new level. Currently this will cause filenames to be logged as they are added to a bundle. To enable maximum verbosity (debug level), use `-vv`. diff --git a/README.md b/README.md index cf4da9df..841ed08a 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ you will need to include the `--cacert` option that points to your certificate authority (CA) trusted certificates file. Both of these options can be saved along with the URL and API Key for a server. -> **Note** +> **Note** > When certificate information is saved for the server, the specified file > is read and its _contents_ are saved under the server's nickname. If the CA file's > contents are ever changed, you will need to add the server information again. @@ -135,7 +135,7 @@ rsconnect add \ --name myserver ``` -> **Note** +> **Note** > The `rsconnect` CLI will verify that the serve URL and API key > are valid. If either is found not to be, no information will be saved. @@ -407,6 +407,35 @@ library(rsconnect) ?rsconnect::writeManifest ``` +### Deploying from Git Repositories +You can deploy content directly from from hosted Git repositories to Posit Connect. +The content must have an existing `manifest.json` file to identify the content +type. For Python content, a `requirements.txt` file must also be present. + +See the [Connect user guide](https://docs.posit.co/connect/user/git-backed/) +for details on how to prepare your content for Git publishing. + +Once your git repository contains the prepared content, use the `deploy git` command: +``` +rsconnect deploy git -r https://my.repository.server/repository +``` + +To deploy from a branch other than `main`, use the `--branch/-b` option. + +To deploy content from a subdirectory, provide the subdirectory +using the `--subdirectory/-d` option. The specified directory +must contain the `manifest.json` file. + +``` +rsconnect deploy git -r https://my.repository.server/repository -b my-branch -d path/within/repo +``` + +These commands create a new git-backed deployment within Posit Connect, +which will periodically check for new commits to your repository/branch +and deploy updates automatically. Do not run the +`deploy git` command again for the same source +unless you want to create a second, separate deployment for it. + ### Options for All Types of Deployments These options apply to any type of content deployment. @@ -430,7 +459,7 @@ filename referenced in the manifest. ### Environment variables You can set environment variables during deployment. Their names and values will be -passed to Posit Connect during deployment so you can use them in your code. Note that +passed to Posit Connect during deployment so you can use them in your code. Note that if you are using `rsconnect` to deploy to shinyapps.io, environment variable management is not supported on that platform. @@ -985,9 +1014,9 @@ xargs printf -- '-g %s\n' < guids.txt | xargs rsconnect content build add ``` ## Programmatic Provisioning -Posit Connect supports the programmatic bootstrapping of an administrator API key +Posit Connect supports the programmatic bootstrapping of an administrator API key for scripted provisioning tasks. This process is supported by the `rsconnect bootstrap` command, -which uses a JSON Web Token to request an initial API key from a fresh Connect instance. +which uses a JSON Web Token to request an initial API key from a fresh Connect instance. > **Warning** > This feature **requires Python version 3.6 or higher**. @@ -998,7 +1027,7 @@ rsconnect bootstrap \ --jwt-keypath /path/to/secret.key ``` -A full description on how to use `rsconnect bootstrap` in a provisioning workflow is provided in the Connect administrator guide's +A full description on how to use `rsconnect bootstrap` in a provisioning workflow is provided in the Connect administrator guide's [programmatic provisioning](https://docs.posit.co/connect/admin/programmatic-provisioning) documentation. ## Server Administration Tasks diff --git a/rsconnect/api.py b/rsconnect/api.py index 9fd55962..cc9a8d23 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -233,6 +233,36 @@ def task_get(self, task_id, first_status=None): self._server.handle_bad_response(response) return response + def deploy_git(self, app_name, repository, branch, subdirectory, app_title, env_vars): + app = self.app_create(app_name) + self._server.handle_bad_response(app) + + resp = self.post( + "applications/%s/repo" % app["guid"], + body={"repository": repository, "branch": branch, "subdirectory": subdirectory}, + ) + self._server.handle_bad_response(resp) + + if app_title: + resp = self.app_update(app["guid"], {"title": app_title}) + self._server.handle_bad_response(resp) + app["title"] = app_title + + if env_vars: + result = self.app_add_environment_vars(app["guid"], list(env_vars.items())) + self._server.handle_bad_response(result) + + task = self.app_deploy(app["guid"]) + self._server.handle_bad_response(task) + + return { + "task_id": task["id"], + "app_id": app["id"], + "app_guid": app["guid"], + "app_url": app["url"], + "title": app["title"], + } + def deploy(self, app_id, app_name, app_title, title_is_default, tarball, env_vars=None): if app_id is None: # create an app if id is not provided @@ -300,7 +330,6 @@ def wait_for_task( poll_wait=0.5, raise_on_error=True, ): - if log_callback is None: log_lines = [] log_callback = log_lines.append @@ -741,6 +770,28 @@ def deploy_bundle( } return self + @cls_logged("Deploying git repository ...") + def deploy_git( + self, + app_name: str = None, + title: str = None, + repository: str = None, + branch: str = None, + subdirectory: str = None, + env_vars: typing.Dict[str, str] = None, + ): + app_name = app_name or self.get("app_name") + repository = repository or self.get("repository") + branch = branch or self.get("branch") + subdirectory = subdirectory or self.get("subdirectory") + title = title or self.get("title") + env_vars = env_vars or self.get("env_vars") + + result = self.client.deploy_git(app_name, repository, branch, subdirectory, title, env_vars) + self.remote_server.handle_bad_response(result) + self.state["deployed_info"] = result + return self + def emit_task_log( self, app_id: int = None, @@ -1331,7 +1382,6 @@ def prepare_deploy( app_mode: AppMode, app_store_version: typing.Optional[int], ) -> PrepareDeployOutputResult: - application_type = "static" if app_mode in [AppModes.STATIC, AppModes.STATIC_QUARTO] else "connect" logger.debug(f"application_type: {application_type}") diff --git a/rsconnect/main.py b/rsconnect/main.py index c10d77fd..79152ae8 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1401,6 +1401,65 @@ def deploy_help(): click.echo() +@deploy.command( + name="git", + short_help="Deploy git repository with exisiting manifest file", + help="Deploy git repository with exisiting manifest file", +) +@server_args +@click.option("--app_name", "-a") +@click.option( + "--repository", + "-r", + required=True, + help="Repository URL to deploy, e.g. https://github.com/username/repository. Only https URLs are supported.", +) +@click.option( + "--branch", + "-b", + default="main", + help=("Name of the branch to deploy. Connect will automatically " + + "deploy updates when commits are pushed to the branch."), +) +@click.option( + "--subdirectory", + "-d", + default="/", + help="Directory within the repository to deploy. The directory must contain a manifest.json file.", +) +@click.option("--title", "-t", help="Title of the content (default is the same as the filename).") +@click.option( + "--environment", + "-E", + "env_vars", + multiple=True, + callback=validate_env_vars, + help="Set an environment variable. Specify a value with NAME=VALUE, " + "or just NAME to use the value from the local environment. " + "May be specified multiple times. [v1.8.6+]", +) +@cli_exception_handler +def deploy_git( + name: str, + server: str, + api_key: str, + insecure: bool, + cacert: typing.IO, + verbose, + app_name: str, + repository: str, + branch: str, + subdirectory: str, + title: str, + env_vars: typing.Dict[str, str], +): + subdirectory = subdirectory.strip("/") + kwargs = locals() + set_verbosity(verbose) + ce = RSConnectExecutor(**kwargs) + ce.validate_server().deploy_git().emit_task_log() + + @cli.group( name="write-manifest", no_args_is_help=True,