Skip to content

Commit ad7de8b

Browse files
committed
fix: add content build run --force flag
Signed-off-by: Lucas Rodriguez <[email protected]>
1 parent 801d8f0 commit ad7de8b

File tree

5 files changed

+145
-23
lines changed

5 files changed

+145
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010

1111
- Added validation for required flags for the `rsconnect system caches delete` command.
12+
- Added `--force` flag to `rsconnect content build run` command. This allows users
13+
to force builds when a build is already marked as running.
1214

1315
## [1.25.0] - 2024-12-18
1416

rsconnect/actions_content.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ def build_add_content(
4949
:param content_guids_with_bundle: Union[tuple[models.ContentGuidWithBundle], list[models.ContentGuidWithBundle]]
5050
"""
5151
build_store = ensure_content_build_store(connect_server)
52-
if build_store.get_build_running():
53-
raise RSConnectException(
54-
"There is already a build running on this server, "
55-
+ "please wait for it to finish before adding new content."
56-
)
57-
5852
with RSConnectClient(connect_server) as client:
5953
if len(content_guids_with_bundle) == 1:
6054
all_content = [client.content_get(content_guids_with_bundle[0].guid)]
@@ -104,10 +98,6 @@ def build_remove_content(
10498
_validate_build_rm_args(guid, all, purge)
10599

106100
build_store = ensure_content_build_store(connect_server)
107-
if build_store.get_build_running():
108-
raise RSConnectException(
109-
"There is a build running on this server, " + "please wait for it to finish before removing content."
110-
)
111101
guids: list[str]
112102
if all:
113103
guids = [c["guid"] for c in build_store.get_content_items()]
@@ -141,10 +131,23 @@ def build_start(
141131
all: bool = False,
142132
poll_wait: int = 1,
143133
debug: bool = False,
134+
force: bool = False,
144135
):
145136
build_store = ensure_content_build_store(connect_server)
146-
if build_store.get_build_running():
147-
raise RSConnectException("There is already a build running on this server: %s" % connect_server.url)
137+
if build_store.get_build_running() and not force:
138+
raise RSConnectException(
139+
"There is already a build running on this server: %s. "
140+
"Use the '--force' flag to override this check." % connect_server.url
141+
)
142+
143+
# prompt the user to confirm that they want to --force a build.
144+
if force:
145+
logger.warning("Please ensure a build is not already running in another terminal before proceeding.")
146+
user_input = input("Are you sure you want to proceed? Type 'yes' to confirm: ").strip().lower()
147+
if user_input != "yes":
148+
logger.warning("Build aborted.")
149+
return
150+
logger.info("Proceeding with the build operation...")
148151

149152
# if we are re-building any already "tracked" content items, then re-add them to be safe
150153
if all:
@@ -154,7 +157,8 @@ def build_start(
154157
build_add_content(connect_server, all_content)
155158
else:
156159
# --retry is shorthand for --aborted --error --running
157-
if retry:
160+
# --force has the same behavior as --retry and also ignores when rsconnect_build_running=true
161+
if retry or force:
158162
aborted = True
159163
error = True
160164
running = True
@@ -277,12 +281,12 @@ def _monitor_build(connect_server: RSConnectServer, content_items: list[ContentI
277281
)
278282

279283
if build_store.aborted():
280-
logger.warn("Build interrupted!")
284+
logger.warning("Build interrupted!")
281285
aborted_builds = [i["guid"] for i in content_items if i["rsconnect_build_status"] == BuildStatus.RUNNING]
282286
if len(aborted_builds) > 0:
283-
logger.warn("Marking %d builds as ABORTED..." % len(aborted_builds))
287+
logger.warning("Marking %d builds as ABORTED..." % len(aborted_builds))
284288
for guid in aborted_builds:
285-
logger.warn("Build aborted: %s" % guid)
289+
logger.warning("Build aborted: %s" % guid)
286290
build_store.set_content_item_build_status(guid, BuildStatus.ABORTED)
287291
return False
288292

rsconnect/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,11 @@ def get_build_logs(
27552755
is_flag=True,
27562756
help="Log stacktraces from exceptions during background operations.",
27572757
)
2758+
@click.option(
2759+
"--force",
2760+
is_flag=True,
2761+
help="Always build content even if a build is already marked as running. Builds the same content as --retry.",
2762+
)
27582763
@click.pass_context
27592764
def start_content_build(
27602765
ctx: click.Context,
@@ -2772,6 +2777,7 @@ def start_content_build(
27722777
poll_wait: int,
27732778
format: LogOutputFormat.All,
27742779
debug: bool,
2780+
force: bool,
27752781
verbose: int,
27762782
):
27772783
set_verbosity(verbose)
@@ -2781,7 +2787,7 @@ def start_content_build(
27812787
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
27822788
if not isinstance(ce.remote_server, RSConnectServer):
27832789
raise RSConnectException("rsconnect content build run` requires a Posit Connect server.")
2784-
build_start(ce.remote_server, parallelism, aborted, error, running, retry, all, poll_wait, debug)
2790+
build_start(ce.remote_server, parallelism, aborted, error, running, retry, all, poll_wait, debug, force)
27852791

27862792

27872793
@cli.group(no_args_is_help=True, help="Interact with Posit Connect's system API.")

rsconnect/utils_package.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def fix_starlette_requirements(
246246
if compare_semvers(starlette_req.specs[0].version, "0.35.0") >= 0:
247247
# starlette is in requirements.txt, but with a version spec that is
248248
# not compatible with this version of Connect.
249-
logger.warn(
249+
logger.warning(
250250
"Starlette version is 0.35.0 or greater, but this version of Connect "
251251
"requires starlette<0.35.0. Setting to <0.35.0."
252252
)

tests/test_main_content.py

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
import tarfile
55
import unittest
6+
from unittest.mock import patch
67

78
import httpretty
89
from click.testing import CliRunner
@@ -11,7 +12,8 @@
1112
from rsconnect import VERSION
1213
from rsconnect.api import RSConnectServer
1314
from rsconnect.models import BuildStatus
14-
from rsconnect.metadata import ContentBuildStore, _normalize_server_url
15+
from rsconnect.actions_content import ensure_content_build_store
16+
from rsconnect.metadata import _normalize_server_url
1517

1618
from .utils import apply_common_args
1719

@@ -98,6 +100,8 @@ def tearDownClass(cls):
98100
def setUp(self):
99101
self.connect_server = "http://localhost:3939"
100102
self.api_key = "testapikey123"
103+
self.build_store = ensure_content_build_store(RSConnectServer(self.connect_server, self.api_key))
104+
self.build_store.set_build_running(False)
101105

102106
def test_version(self):
103107
runner = CliRunner()
@@ -218,10 +222,9 @@ def test_build_retry(self):
218222
self.assertTrue(os.path.exists("%s/%s.json" % (TEMP_DIR, _normalize_server_url(self.connect_server))))
219223

220224
# change the content build status so it looks like it was interrupted/failed
221-
store = ContentBuildStore(RSConnectServer(self.connect_server, self.api_key))
222-
store.set_content_item_build_status("7d59c5c7-c4a7-4950-acc3-3943b7192bc4", BuildStatus.RUNNING)
223-
store.set_content_item_build_status("ab497e4b-b706-4ae7-be49-228979a95eb4", BuildStatus.ABORTED)
224-
store.set_content_item_build_status("cdfed1f7-0e09-40eb-996d-0ef77ea2d797", BuildStatus.ERROR)
225+
self.build_store.set_content_item_build_status("7d59c5c7-c4a7-4950-acc3-3943b7192bc4", BuildStatus.RUNNING)
226+
self.build_store.set_content_item_build_status("ab497e4b-b706-4ae7-be49-228979a95eb4", BuildStatus.ABORTED)
227+
self.build_store.set_content_item_build_status("cdfed1f7-0e09-40eb-996d-0ef77ea2d797", BuildStatus.ERROR)
225228

226229
# run the build
227230
args = ["content", "build", "run", "--retry"]
@@ -250,6 +253,113 @@ def test_build_retry(self):
250253
self.assertEqual(listing[1]["rsconnect_build_status"], BuildStatus.COMPLETE)
251254
self.assertEqual(listing[2]["rsconnect_build_status"], BuildStatus.COMPLETE)
252255

256+
@httpretty.activate(verbose=True, allow_net_connect=False)
257+
def test_build_already_running_error(self):
258+
register_uris(self.connect_server)
259+
runner = CliRunner()
260+
261+
args = ["content", "build", "add", "-g", "7d59c5c7-c4a7-4950-acc3-3943b7192bc4"]
262+
apply_common_args(args, server=self.connect_server, key=self.api_key)
263+
result = runner.invoke(cli, args)
264+
self.assertEqual(result.exit_code, 0, result.output)
265+
self.assertTrue(os.path.exists("%s/%s.json" % (TEMP_DIR, _normalize_server_url(self.connect_server))))
266+
267+
# set rsconnect_build_running to true to trigger "already a build running" error
268+
self.build_store.set_build_running(True)
269+
270+
# build without --force flag should fail
271+
args = ["content", "build", "run"]
272+
apply_common_args(args, server=self.connect_server, key=self.api_key)
273+
result = runner.invoke(cli, args)
274+
self.assertEqual(result.exit_code, 1)
275+
self.assertRegex(result.output, "There is already a build running on this server")
276+
self.assertRegex(result.output, "Use the '--force' flag to override this check")
277+
278+
@httpretty.activate(verbose=True, allow_net_connect=False)
279+
def test_build_force_abort(self):
280+
register_uris(self.connect_server)
281+
runner = CliRunner()
282+
283+
args = ["content", "build", "add", "-g", "7d59c5c7-c4a7-4950-acc3-3943b7192bc4"]
284+
apply_common_args(args, server=self.connect_server, key=self.api_key)
285+
result = runner.invoke(cli, args)
286+
self.assertEqual(result.exit_code, 0, result.output)
287+
self.assertTrue(os.path.exists("%s/%s.json" % (TEMP_DIR, _normalize_server_url(self.connect_server))))
288+
289+
# set rsconnect_build_running to true
290+
# --force flag should ignore this and not fail.
291+
self.build_store.set_build_running(True)
292+
293+
# mock "no" input to simulate user response to prompt
294+
with patch("builtins.input", return_value="no"), self.assertLogs("rsconnect") as log:
295+
args = ["content", "build", "run", "--force"]
296+
apply_common_args(args, server=self.connect_server, key=self.api_key)
297+
result = runner.invoke(cli, args)
298+
self.assertEqual(result.exit_code, 0)
299+
self.assertIn("Please ensure a build is not already running in another terminal", log.output[0])
300+
self.assertIn("Build aborted", log.output[1])
301+
302+
@httpretty.activate(verbose=True, allow_net_connect=False)
303+
def test_build_force_success(self):
304+
register_uris(self.connect_server)
305+
runner = CliRunner()
306+
307+
# add 3 content items
308+
args = [
309+
"content",
310+
"build",
311+
"add",
312+
"-g",
313+
"7d59c5c7-c4a7-4950-acc3-3943b7192bc4",
314+
"-g",
315+
"ab497e4b-b706-4ae7-be49-228979a95eb4",
316+
"-g",
317+
"cdfed1f7-0e09-40eb-996d-0ef77ea2d797",
318+
]
319+
apply_common_args(args, server=self.connect_server, key=self.api_key)
320+
result = runner.invoke(cli, args)
321+
self.assertEqual(result.exit_code, 0, result.output)
322+
self.assertTrue(os.path.exists("%s/%s.json" % (TEMP_DIR, _normalize_server_url(self.connect_server))))
323+
324+
# change the content build status so it looks like it was interrupted/failed
325+
self.build_store.set_content_item_build_status("7d59c5c7-c4a7-4950-acc3-3943b7192bc4", BuildStatus.RUNNING)
326+
self.build_store.set_content_item_build_status("ab497e4b-b706-4ae7-be49-228979a95eb4", BuildStatus.ABORTED)
327+
self.build_store.set_content_item_build_status("cdfed1f7-0e09-40eb-996d-0ef77ea2d797", BuildStatus.ERROR)
328+
329+
# set rsconnect_build_running to true
330+
# --force flag should ignore this and not fail.
331+
self.build_store.set_build_running(True)
332+
333+
# mock "yes" input to simulate user response to prompt
334+
with patch("builtins.input", return_value="yes"), self.assertLogs("rsconnect") as log:
335+
args = ["content", "build", "run", "--force"]
336+
apply_common_args(args, server=self.connect_server, key=self.api_key)
337+
result = runner.invoke(cli, args)
338+
self.assertEqual(result.exit_code, 0)
339+
self.assertIn("Please ensure a build is not already running in another terminal", log.output[0])
340+
self.assertIn("Proceeding with the build operation...", log.output[1])
341+
342+
# check that the build succeeded
343+
args = [
344+
"content",
345+
"build",
346+
"ls",
347+
"-g",
348+
"7d59c5c7-c4a7-4950-acc3-3943b7192bc4",
349+
"-g",
350+
"ab497e4b-b706-4ae7-be49-228979a95eb4",
351+
"-g",
352+
"cdfed1f7-0e09-40eb-996d-0ef77ea2d797",
353+
]
354+
apply_common_args(args, server=self.connect_server, key=self.api_key)
355+
result = runner.invoke(cli, args)
356+
self.assertEqual(result.exit_code, 0, result.output)
357+
listing = json.loads(result.output)
358+
self.assertTrue(len(listing) == 3)
359+
self.assertEqual(listing[0]["rsconnect_build_status"], BuildStatus.COMPLETE)
360+
self.assertEqual(listing[1]["rsconnect_build_status"], BuildStatus.COMPLETE)
361+
self.assertEqual(listing[2]["rsconnect_build_status"], BuildStatus.COMPLETE)
362+
253363
@httpretty.activate(verbose=True, allow_net_connect=False)
254364
def test_build_rm(self):
255365
register_uris(self.connect_server)

0 commit comments

Comments
 (0)