Skip to content

Commit fda4cb2

Browse files
Add Posit Publisher .posit/publish TOML interoperability
Read and write Publisher's .posit/publish config + deployment record files alongside the legacy rsconnect-python JSON store, so content can be published with either tool. - New rsconnect/publisher package (schema, serialize, config, record, store) porting Publisher's format: content-type map, $schema-first TOML with multiline arrays, and the random base-32 file-naming methodology. - Dual-write .posit on Connect/SPCS deploys via save_deployed_info (best-effort). - New 'rsconnect redeploy [PATH]' command driven by .posit, with a fallback to manifest.json + legacy rsconnect-python/*.json for pre-.posit content. - 'write-manifest' commands also emit a .posit config. - Connect Cloud files are read/preserved for interop but not deployable here. - Adds tomli-w dependency; tests in tests/test_publisher.py and test_redeploy.py.
1 parent bc68be5 commit fda4cb2

14 files changed

Lines changed: 2628 additions & 529 deletions

File tree

conftest.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import glob
12
import os
3+
import shutil
24
import sys
35

4-
from os.path import abspath, dirname
6+
from os.path import abspath, dirname, join
7+
8+
import pytest
59

610

711
HERE = dirname(abspath(__file__))
@@ -12,3 +16,24 @@
1216
# default argument value at import time, so this must be set before any test
1317
# module imports rsconnect. (Previously injected by the Makefile's TEST_ENV.)
1418
os.environ.setdefault("CONNECT_CONTENT_BUILD_DIR", "rsconnect-build-test")
19+
20+
_TESTDATA = join(HERE, "tests", "testdata")
21+
22+
23+
def _remove_stray_posit_dirs():
24+
"""Delete any ``.posit`` directories under tests/testdata.
25+
26+
Deploying or writing a manifest for a directory now emits Posit Publisher
27+
``.posit/publish`` files next to the content. Tests that run those flows
28+
against the shared testdata fixtures would otherwise leave stray artifacts in
29+
the working tree; no ``.posit`` fixtures are committed there.
30+
"""
31+
for path in glob.glob(join(_TESTDATA, "**", ".posit"), recursive=True):
32+
shutil.rmtree(path, ignore_errors=True)
33+
34+
35+
@pytest.fixture(scope="session", autouse=True)
36+
def _clean_publisher_artifacts():
37+
_remove_stray_posit_dirs()
38+
yield
39+
_remove_stray_posit_dirs()

docs/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Added interoperability with Posit Publisher's `.posit/publish` project files.
11+
When deploying to Posit Connect or Snowflake (SPCS), rsconnect-python now
12+
writes a Publisher configuration (`.posit/publish/<name>.toml`) and deployment
13+
record (`.posit/publish/deployments/<name>.toml`) alongside the existing
14+
`rsconnect-python/` metadata, so the same project can be published with either
15+
tool. Publisher-authored configurations and records are read and preserved.
16+
- Added a `rsconnect redeploy [PATH]` command that redeploys content using an
17+
existing `.posit/publish` project, recovering the target server and content
18+
identity from the deployment record so no framework, entrypoint, or server
19+
needs to be specified. `PATH` defaults to the current directory. When a
20+
project predates `.posit` but has a `manifest.json` and a legacy
21+
`rsconnect-python/` deployment record, `redeploy` falls back to those and
22+
writes `.posit` files going forward.
23+
- The `rsconnect write-manifest` commands now also write a `.posit/publish`
24+
configuration next to the generated `manifest.json`.
25+
- Connect Cloud (`connect.posit.cloud`) `.posit` files are read and preserved
26+
for interoperability, but deploying to Connect Cloud is not supported by this
27+
tool; only Posit Connect and Snowflake (SPCS) targets write `.posit` metadata.
28+
1029
## [1.30.0] - 2026-07-16
1130

1231
- Fixed a bug where `rsconnect deploy notebook --static` failed with `Unable to

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies = [
1717
"click>=8.0.0",
1818
"packaging>=20.0",
1919
"toml>=0.10; python_version < '3.11'",
20+
"tomli-w>=1.0.0",
2021
]
2122

2223
[project.scripts]

rsconnect/api.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,8 +1794,45 @@ def save_deployed_info(self):
17941794
self.app_mode,
17951795
)
17961796

1797+
# Dual-write Posit Publisher's .posit/publish config + deployment record
1798+
# for Connect/SPCS targets so the two tools interoperate. shinyapps.io /
1799+
# Posit Cloud (which lack a content GUID and dashboard URLs) stay on the
1800+
# legacy JSON store only.
1801+
if isinstance(self.remote_server, (RSConnectServer, SPCSConnectServer)):
1802+
self._save_publisher_metadata(deployed_info)
1803+
17971804
return self
17981805

1806+
def _save_publisher_metadata(self, deployed_info: RSConnectClientDeployResult):
1807+
"""Best-effort write of the ``.posit/publish`` config + record.
1808+
1809+
The deploy has already succeeded by the time metadata is saved, so any
1810+
failure here warns rather than aborting (mirroring the legacy save)."""
1811+
if self.bundle is None:
1812+
return
1813+
try:
1814+
from .publisher import schema
1815+
from .publisher.store import write_deployment_metadata
1816+
1817+
path = self.path
1818+
project_dir = path if os.path.isdir(path) else os.path.dirname(abspath(path))
1819+
product_type = (
1820+
schema.PRODUCT_TYPE_SNOWFLAKE
1821+
if isinstance(self.remote_server, SPCSConnectServer)
1822+
else schema.PRODUCT_TYPE_CONNECT
1823+
)
1824+
write_deployment_metadata(
1825+
project_dir=project_dir,
1826+
server_url=self.remote_server.url,
1827+
product_type=product_type,
1828+
app_mode=self.app_mode or AppModes.UNKNOWN,
1829+
title=deployed_info.get("title") or self.title,
1830+
deployed_info=deployed_info,
1831+
bundle=self.bundle,
1832+
)
1833+
except Exception as e:
1834+
logger.warning("Could not write .posit/publish metadata: %s", e)
1835+
17991836
@property
18001837
def supports_verify_before_activate(self) -> bool:
18011838
"""Whether the target server supports deploying a bundle as a draft and

0 commit comments

Comments
 (0)