From 44164f333deff781385d147d9058ba397ccdcd7f Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Tue, 4 Aug 2026 18:14:25 -0400 Subject: [PATCH 1/2] fix: parse rsconnect-args with shell quoting so values can contain spaces deploy.sh expanded rsconnect-args unquoted, so plain Bash word-splitting turned `--title "My App"` into three tokens (including a stray literal quote), which the click-based CLI rejected. Parse the raw input with shlex.split (POSIX rules) in a new connect_actions.rsconnect_args module, expose it via a resolve-rsconnect-args CLI subcommand that writes a newline-delimited GITHUB_OUTPUT value (mirroring extra_files), and have deploy.sh read it into an array and expand it quoted, same as EXTRA_FILES. Simple space-separated args behave exactly as before. Fixes #79 Co-Authored-By: Claude Fable 5 --- README.md | 2 +- deploy/action.yml | 14 +++++++- deploy/scripts/deploy.sh | 18 ++++++++-- src/connect_actions/cli.py | 26 +++++++++++++++ src/connect_actions/rsconnect_args.py | 43 ++++++++++++++++++++++++ tests/test_cli.py | 41 +++++++++++++++++++++++ tests/test_rsconnect_args.py | 47 +++++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/connect_actions/rsconnect_args.py create mode 100644 tests/test_rsconnect_args.py diff --git a/README.md b/README.md index e6158fa..b501e90 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Here are the full list of inputs and outputs; below we describe what exactly is | `path` | No | Path to the application directory within the repository. Defaults to the repository root. Use this when your app lives in a subdirectory of your repo. | | `draft` | No | Deploy as a draft (preview) bundle instead of activating it. Defaults to `true` on `pull_request` events and `false` otherwise. Set it explicitly to override--e.g. `false` to publish directly from a PR, or `true` to stage a draft from a push. | | `github-token` | No | GitHub token for commenting preview URLs on PRs | -| `rsconnect-args` | No | Additional arguments passed to `rsconnect deploy` | +| `rsconnect-args` | No | Additional arguments passed to `rsconnect deploy`. Supports shell-style quoting for values containing spaces, e.g. `--title "My App"`. | #### Outputs diff --git a/deploy/action.yml b/deploy/action.yml index 6d28ef6..ec3e609 100644 --- a/deploy/action.yml +++ b/deploy/action.yml @@ -125,6 +125,18 @@ runs: working-directory: ${{ inputs.path }} run: ${{ github.action_path }}/scripts/generate-requirements.sh + # Splits rsconnect-args with shell quoting rules (Python shlex.split) so a + # quoted value containing spaces (e.g. `--title "My App"`) survives as a + # single argument instead of being IFS word-split by Bash. Emitted as a + # newline-delimited output (like extra_files) for deploy.sh to read into + # an array and expand quoted. + - name: Parse rsconnect-args + id: rsconnect_args + shell: bash + run: uv run --project ${{ github.action_path }}/.. python -m connect_actions.cli resolve-rsconnect-args + env: + INPUT_RSCONNECT_ARGS: ${{ inputs.rsconnect-args }} + - name: Deploy to Connect id: deploy shell: bash @@ -146,7 +158,7 @@ runs: PR_NUMBER: ${{ github.event.pull_request.number || '' }} PR_TITLE: ${{ github.event.pull_request.title || '' }} COMMIT_MESSAGE: ${{ github.event.head_commit.message || '' }} - RSCONNECT_ARGS: ${{ inputs.rsconnect-args }} + RSCONNECT_ARGS: ${{ steps.rsconnect_args.outputs.rsconnect_args }} - name: Comment preview URL on PR if: github.event_name == 'pull_request' && inputs.draft == 'true' diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh index c040d7a..05189ca 100755 --- a/deploy/scripts/deploy.sh +++ b/deploy/scripts/deploy.sh @@ -5,7 +5,8 @@ # Required env vars: CONTENT_GUID, APP_TYPE (resolved by the "Determine app type" # step: a `posit connect deploy` subcommand, or "manifest") # Optional env vars: CONFIG_ENTRYPOINT, EXTRA_FILES, DRAFT, GITHUB_EVENT_NAME, -# RSCONNECT_ARGS +# RSCONNECT_ARGS (newline-delimited, resolved from the rsconnect-args input +# by the "Parse rsconnect-args" step so quoted values may contain spaces) set -euo pipefail @@ -99,8 +100,19 @@ if [ "${SEND_METADATA:-true}" = "true" ]; then fi fi -# shellcheck disable=SC2086 -posit connect deploy "$APP_TYPE" "${DRAFT_ARGS[@]}" --app-id "$CONTENT_GUID" "${ENTRYPOINT_ARGS[@]}" "${METADATA_ARGS[@]}" ${RSCONNECT_ARGS:-} "$DEPLOY_TARGET" "${EXTRA_FILE_ARGS[@]}" 2>&1 | tee deploy.log +# RSCONNECT_ARGS is newline-delimited (already split with shell quoting rules +# by the "Parse rsconnect-args" step), so read it into an array the same way +# EXTRA_FILES is above; this lets a quoted value with spaces (e.g. +# `--title "My App"`) reach the CLI as a single argument instead of being +# IFS word-split by Bash. +RSCONNECT_ARGS_ARR=() +if [ -n "${RSCONNECT_ARGS:-}" ]; then + while IFS= read -r rsconnect_arg; do + [ -n "$rsconnect_arg" ] && RSCONNECT_ARGS_ARR+=("$rsconnect_arg") + done <<< "$RSCONNECT_ARGS" +fi + +posit connect deploy "$APP_TYPE" "${DRAFT_ARGS[@]}" --app-id "$CONTENT_GUID" "${ENTRYPOINT_ARGS[@]}" "${METADATA_ARGS[@]}" "${RSCONNECT_ARGS_ARR[@]}" "$DEPLOY_TARGET" "${EXTRA_FILE_ARGS[@]}" 2>&1 | tee deploy.log # Extract URL from logs, stripping ANSI color codes CONTENT_URL=$(grep "$URL_PATTERN" deploy.log | sed "s/.*$URL_PATTERN //" | sed 's/\x1b\[[0-9;]*m//g') diff --git a/src/connect_actions/cli.py b/src/connect_actions/cli.py index 7d33a50..00bad74 100644 --- a/src/connect_actions/cli.py +++ b/src/connect_actions/cli.py @@ -13,6 +13,7 @@ from .apptype import AppTypeError, resolve_app_type from .config import ConfigError, resolve_config +from .rsconnect_args import RsconnectArgsError, parse_rsconnect_args from .versions import format_min_version, supports @@ -133,6 +134,25 @@ def cmd_check_deploy_features(_args: argparse.Namespace) -> int: return 0 +def cmd_resolve_rsconnect_args(_args: argparse.Namespace) -> int: + """Split the raw ``rsconnect-args`` input into a shell-quoted argument list. + + Reads ``INPUT_RSCONNECT_ARGS`` and writes it back as a newline-delimited + ``rsconnect_args`` output (the same pattern ``extra_files`` uses), so + ``deploy.sh`` can read it into a Bash array and expand each argument + quoted instead of relying on unquoted (IFS) word-splitting, which can't + express a value containing whitespace (e.g. ``--title "My App"``). + """ + try: + args = parse_rsconnect_args(os.environ.get("INPUT_RSCONNECT_ARGS", "")) + except RsconnectArgsError as err: + print(f"Error: {err}", file=sys.stderr) + return 1 + + _write_output(rsconnect_args="\n".join(args)) + return 0 + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="connect_actions") subparsers = parser.add_subparsers(dest="command", required=True) @@ -155,6 +175,12 @@ def main(argv: list[str] | None = None) -> int: ) deploy_features.set_defaults(func=cmd_check_deploy_features) + rsconnect_args = subparsers.add_parser( + "resolve-rsconnect-args", + help="Split the rsconnect-args input into a shell-quoted argument list.", + ) + rsconnect_args.set_defaults(func=cmd_resolve_rsconnect_args) + args = parser.parse_args(argv) return args.func(args) diff --git a/src/connect_actions/rsconnect_args.py b/src/connect_actions/rsconnect_args.py new file mode 100644 index 0000000..e564697 --- /dev/null +++ b/src/connect_actions/rsconnect_args.py @@ -0,0 +1,43 @@ +"""Parse the ``rsconnect-args`` action input with shell quoting rules. + +``rsconnect-args`` lets a caller pass arbitrary extra flags through to +``posit connect deploy`` (e.g. ``--title "My App"``). Passing the raw string +straight to Bash for unquoted (IFS) word-splitting can't express a value +containing whitespace: ``--title "My App"`` would split into three tokens -- +``--title``, ``"My`` (literal quote included), and ``App"`` -- and the +click-based CLI rejects the extra argument. + +:func:`parse_rsconnect_args` instead splits the string with POSIX shell +quoting rules (:func:`shlex.split`), so quoted substrings stay together as a +single argument while plain space-separated args (e.g. ``--verbose --new``) +behave exactly as before. The thin CLI layer in :mod:`connect_actions.cli` +writes the resulting list as a newline-delimited ``GITHUB_OUTPUT`` value (the +same pattern ``extra_files`` uses), and ``deploy.sh`` reads it back into a +Bash array to expand with proper quoting. +""" + +from __future__ import annotations + +import shlex + + +class RsconnectArgsError(Exception): + """Raised when ``rsconnect-args`` can't be parsed as a shell-quoted string. + + The message matches what the action prints (without the ``Error: `` + prefix, which the CLI layer adds). + """ + + +def parse_rsconnect_args(raw: str) -> list[str]: + """Split the raw ``rsconnect-args`` string into individual arguments. + + Uses POSIX shell quoting rules, so ``--title "My App"`` yields + ``["--title", "My App"]`` while unquoted args split on whitespace exactly + as unquoted Bash expansion would. Raises :class:`RsconnectArgsError` on + malformed input (e.g. an unbalanced quote). + """ + try: + return shlex.split(raw) + except ValueError as err: + raise RsconnectArgsError(f"Could not parse rsconnect-args: {err}") from err diff --git a/tests/test_cli.py b/tests/test_cli.py index 6a1d5cf..22bb5e7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -168,3 +168,44 @@ def test_check_deploy_features_unknown_version_skips_metadata(tmp_path, monkeypa assert main(["check-deploy-features"]) == 0 assert "send_metadata=false" in output_file.read_text() assert "Could not determine the Connect server version" in capsys.readouterr().out + + +def test_resolve_rsconnect_args_simple_flags(tmp_path, monkeypatch): + output_file = tmp_path / "github_output" + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setenv("INPUT_RSCONNECT_ARGS", "--verbose --new") + + assert main(["resolve-rsconnect-args"]) == 0 + + assert "rsconnect_args<<__GHA_EOF__\n--verbose\n--new\n__GHA_EOF__" in output_file.read_text() + + +def test_resolve_rsconnect_args_quoted_value_with_spaces(tmp_path, monkeypatch): + output_file = tmp_path / "github_output" + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setenv("INPUT_RSCONNECT_ARGS", '--title "My App"') + + assert main(["resolve-rsconnect-args"]) == 0 + + written = output_file.read_text() + # The quoted value survives as a single line so deploy.sh reads it back as + # one argument instead of splitting on the embedded space. + assert "rsconnect_args<<__GHA_EOF__\n--title\nMy App\n__GHA_EOF__" in written + + +def test_resolve_rsconnect_args_empty_uses_plain_form(tmp_path, monkeypatch): + output_file = tmp_path / "github_output" + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setenv("INPUT_RSCONNECT_ARGS", "") + + assert main(["resolve-rsconnect-args"]) == 0 + + assert "rsconnect_args=\n" in output_file.read_text() + + +def test_resolve_rsconnect_args_unbalanced_quote_exits_nonzero(tmp_path, monkeypatch, capsys): + monkeypatch.setenv("GITHUB_OUTPUT", str(tmp_path / "github_output")) + monkeypatch.setenv("INPUT_RSCONNECT_ARGS", '--title "My App') + + assert main(["resolve-rsconnect-args"]) == 1 + assert "Error: Could not parse rsconnect-args" in capsys.readouterr().err diff --git a/tests/test_rsconnect_args.py b/tests/test_rsconnect_args.py new file mode 100644 index 0000000..4923a91 --- /dev/null +++ b/tests/test_rsconnect_args.py @@ -0,0 +1,47 @@ +"""Tests for parsing the rsconnect-args input with shell quoting rules.""" + +from __future__ import annotations + +import pytest + +from connect_actions.rsconnect_args import RsconnectArgsError, parse_rsconnect_args + + +def test_empty_string_yields_no_args(): + assert parse_rsconnect_args("") == [] + + +def test_simple_space_separated_args_unchanged(): + # Backward compatibility: plain flags with no quoting behave exactly as + # the old unquoted Bash word-splitting did. + assert parse_rsconnect_args("--verbose --new") == ["--verbose", "--new"] + + +def test_quoted_value_with_spaces_stays_together(): + assert parse_rsconnect_args('--title "My App"') == ["--title", "My App"] + + +def test_single_quoted_value_with_spaces_stays_together(): + assert parse_rsconnect_args("--title 'My App'") == ["--title", "My App"] + + +def test_mixed_quoted_and_unquoted_args(): + assert parse_rsconnect_args('--verbose --title "My App" --new') == [ + "--verbose", + "--title", + "My App", + "--new", + ] + + +def test_extra_whitespace_between_args_is_ignored(): + assert parse_rsconnect_args(" --verbose --new ") == ["--verbose", "--new"] + + +def test_unbalanced_quote_raises_rsconnect_args_error(): + with pytest.raises(RsconnectArgsError, match="Could not parse rsconnect-args"): + parse_rsconnect_args('--title "My App') + + +def test_single_argument_value_with_equals_and_spaces(): + assert parse_rsconnect_args('--title="My App"') == ["--title=My App"] From b5fcc2956a9ac11568a59e90572a7b307a55d5f8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:58:45 +0000 Subject: [PATCH 2/2] simplify: inline rsconnect-args parsing in deploy.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the separate rsconnect_args module, CLI subcommand, action step, and dedicated tests. Instead, deploy.sh parses the raw input inline with a python3 shlex.split one-liner — same shell-quoting behavior, ~170 fewer lines. Co-authored-by: Neal Richardson <2975928+nealrichardson@users.noreply.github.com> --- README.md | 2 +- deploy/action.yml | 14 +------- deploy/scripts/deploy.sh | 17 +++++----- src/connect_actions/cli.py | 26 --------------- src/connect_actions/rsconnect_args.py | 43 ------------------------ tests/test_cli.py | 41 ----------------------- tests/test_rsconnect_args.py | 47 --------------------------- 7 files changed, 11 insertions(+), 179 deletions(-) delete mode 100644 src/connect_actions/rsconnect_args.py delete mode 100644 tests/test_rsconnect_args.py diff --git a/README.md b/README.md index b501e90..117815a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Here are the full list of inputs and outputs; below we describe what exactly is | `path` | No | Path to the application directory within the repository. Defaults to the repository root. Use this when your app lives in a subdirectory of your repo. | | `draft` | No | Deploy as a draft (preview) bundle instead of activating it. Defaults to `true` on `pull_request` events and `false` otherwise. Set it explicitly to override--e.g. `false` to publish directly from a PR, or `true` to stage a draft from a push. | | `github-token` | No | GitHub token for commenting preview URLs on PRs | -| `rsconnect-args` | No | Additional arguments passed to `rsconnect deploy`. Supports shell-style quoting for values containing spaces, e.g. `--title "My App"`. | +| `rsconnect-args` | No | Additional arguments passed to `rsconnect deploy`. | #### Outputs diff --git a/deploy/action.yml b/deploy/action.yml index ec3e609..6d28ef6 100644 --- a/deploy/action.yml +++ b/deploy/action.yml @@ -125,18 +125,6 @@ runs: working-directory: ${{ inputs.path }} run: ${{ github.action_path }}/scripts/generate-requirements.sh - # Splits rsconnect-args with shell quoting rules (Python shlex.split) so a - # quoted value containing spaces (e.g. `--title "My App"`) survives as a - # single argument instead of being IFS word-split by Bash. Emitted as a - # newline-delimited output (like extra_files) for deploy.sh to read into - # an array and expand quoted. - - name: Parse rsconnect-args - id: rsconnect_args - shell: bash - run: uv run --project ${{ github.action_path }}/.. python -m connect_actions.cli resolve-rsconnect-args - env: - INPUT_RSCONNECT_ARGS: ${{ inputs.rsconnect-args }} - - name: Deploy to Connect id: deploy shell: bash @@ -158,7 +146,7 @@ runs: PR_NUMBER: ${{ github.event.pull_request.number || '' }} PR_TITLE: ${{ github.event.pull_request.title || '' }} COMMIT_MESSAGE: ${{ github.event.head_commit.message || '' }} - RSCONNECT_ARGS: ${{ steps.rsconnect_args.outputs.rsconnect_args }} + RSCONNECT_ARGS: ${{ inputs.rsconnect-args }} - name: Comment preview URL on PR if: github.event_name == 'pull_request' && inputs.draft == 'true' diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh index 05189ca..68dab8f 100755 --- a/deploy/scripts/deploy.sh +++ b/deploy/scripts/deploy.sh @@ -5,8 +5,7 @@ # Required env vars: CONTENT_GUID, APP_TYPE (resolved by the "Determine app type" # step: a `posit connect deploy` subcommand, or "manifest") # Optional env vars: CONFIG_ENTRYPOINT, EXTRA_FILES, DRAFT, GITHUB_EVENT_NAME, -# RSCONNECT_ARGS (newline-delimited, resolved from the rsconnect-args input -# by the "Parse rsconnect-args" step so quoted values may contain spaces) +# RSCONNECT_ARGS (raw rsconnect-args input, parsed with shlex for shell quoting) set -euo pipefail @@ -100,16 +99,18 @@ if [ "${SEND_METADATA:-true}" = "true" ]; then fi fi -# RSCONNECT_ARGS is newline-delimited (already split with shell quoting rules -# by the "Parse rsconnect-args" step), so read it into an array the same way -# EXTRA_FILES is above; this lets a quoted value with spaces (e.g. -# `--title "My App"`) reach the CLI as a single argument instead of being -# IFS word-split by Bash. RSCONNECT_ARGS_ARR=() if [ -n "${RSCONNECT_ARGS:-}" ]; then while IFS= read -r rsconnect_arg; do [ -n "$rsconnect_arg" ] && RSCONNECT_ARGS_ARR+=("$rsconnect_arg") - done <<< "$RSCONNECT_ARGS" + done < <(python3 -c " +import shlex, sys +try: + print('\n'.join(shlex.split(sys.argv[1]))) +except ValueError as e: + print(f'Error: Could not parse rsconnect-args: {e}', file=sys.stderr) + sys.exit(1) +" "$RSCONNECT_ARGS") fi posit connect deploy "$APP_TYPE" "${DRAFT_ARGS[@]}" --app-id "$CONTENT_GUID" "${ENTRYPOINT_ARGS[@]}" "${METADATA_ARGS[@]}" "${RSCONNECT_ARGS_ARR[@]}" "$DEPLOY_TARGET" "${EXTRA_FILE_ARGS[@]}" 2>&1 | tee deploy.log diff --git a/src/connect_actions/cli.py b/src/connect_actions/cli.py index 00bad74..7d33a50 100644 --- a/src/connect_actions/cli.py +++ b/src/connect_actions/cli.py @@ -13,7 +13,6 @@ from .apptype import AppTypeError, resolve_app_type from .config import ConfigError, resolve_config -from .rsconnect_args import RsconnectArgsError, parse_rsconnect_args from .versions import format_min_version, supports @@ -134,25 +133,6 @@ def cmd_check_deploy_features(_args: argparse.Namespace) -> int: return 0 -def cmd_resolve_rsconnect_args(_args: argparse.Namespace) -> int: - """Split the raw ``rsconnect-args`` input into a shell-quoted argument list. - - Reads ``INPUT_RSCONNECT_ARGS`` and writes it back as a newline-delimited - ``rsconnect_args`` output (the same pattern ``extra_files`` uses), so - ``deploy.sh`` can read it into a Bash array and expand each argument - quoted instead of relying on unquoted (IFS) word-splitting, which can't - express a value containing whitespace (e.g. ``--title "My App"``). - """ - try: - args = parse_rsconnect_args(os.environ.get("INPUT_RSCONNECT_ARGS", "")) - except RsconnectArgsError as err: - print(f"Error: {err}", file=sys.stderr) - return 1 - - _write_output(rsconnect_args="\n".join(args)) - return 0 - - def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="connect_actions") subparsers = parser.add_subparsers(dest="command", required=True) @@ -175,12 +155,6 @@ def main(argv: list[str] | None = None) -> int: ) deploy_features.set_defaults(func=cmd_check_deploy_features) - rsconnect_args = subparsers.add_parser( - "resolve-rsconnect-args", - help="Split the rsconnect-args input into a shell-quoted argument list.", - ) - rsconnect_args.set_defaults(func=cmd_resolve_rsconnect_args) - args = parser.parse_args(argv) return args.func(args) diff --git a/src/connect_actions/rsconnect_args.py b/src/connect_actions/rsconnect_args.py deleted file mode 100644 index e564697..0000000 --- a/src/connect_actions/rsconnect_args.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Parse the ``rsconnect-args`` action input with shell quoting rules. - -``rsconnect-args`` lets a caller pass arbitrary extra flags through to -``posit connect deploy`` (e.g. ``--title "My App"``). Passing the raw string -straight to Bash for unquoted (IFS) word-splitting can't express a value -containing whitespace: ``--title "My App"`` would split into three tokens -- -``--title``, ``"My`` (literal quote included), and ``App"`` -- and the -click-based CLI rejects the extra argument. - -:func:`parse_rsconnect_args` instead splits the string with POSIX shell -quoting rules (:func:`shlex.split`), so quoted substrings stay together as a -single argument while plain space-separated args (e.g. ``--verbose --new``) -behave exactly as before. The thin CLI layer in :mod:`connect_actions.cli` -writes the resulting list as a newline-delimited ``GITHUB_OUTPUT`` value (the -same pattern ``extra_files`` uses), and ``deploy.sh`` reads it back into a -Bash array to expand with proper quoting. -""" - -from __future__ import annotations - -import shlex - - -class RsconnectArgsError(Exception): - """Raised when ``rsconnect-args`` can't be parsed as a shell-quoted string. - - The message matches what the action prints (without the ``Error: `` - prefix, which the CLI layer adds). - """ - - -def parse_rsconnect_args(raw: str) -> list[str]: - """Split the raw ``rsconnect-args`` string into individual arguments. - - Uses POSIX shell quoting rules, so ``--title "My App"`` yields - ``["--title", "My App"]`` while unquoted args split on whitespace exactly - as unquoted Bash expansion would. Raises :class:`RsconnectArgsError` on - malformed input (e.g. an unbalanced quote). - """ - try: - return shlex.split(raw) - except ValueError as err: - raise RsconnectArgsError(f"Could not parse rsconnect-args: {err}") from err diff --git a/tests/test_cli.py b/tests/test_cli.py index 22bb5e7..6a1d5cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -168,44 +168,3 @@ def test_check_deploy_features_unknown_version_skips_metadata(tmp_path, monkeypa assert main(["check-deploy-features"]) == 0 assert "send_metadata=false" in output_file.read_text() assert "Could not determine the Connect server version" in capsys.readouterr().out - - -def test_resolve_rsconnect_args_simple_flags(tmp_path, monkeypatch): - output_file = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) - monkeypatch.setenv("INPUT_RSCONNECT_ARGS", "--verbose --new") - - assert main(["resolve-rsconnect-args"]) == 0 - - assert "rsconnect_args<<__GHA_EOF__\n--verbose\n--new\n__GHA_EOF__" in output_file.read_text() - - -def test_resolve_rsconnect_args_quoted_value_with_spaces(tmp_path, monkeypatch): - output_file = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) - monkeypatch.setenv("INPUT_RSCONNECT_ARGS", '--title "My App"') - - assert main(["resolve-rsconnect-args"]) == 0 - - written = output_file.read_text() - # The quoted value survives as a single line so deploy.sh reads it back as - # one argument instead of splitting on the embedded space. - assert "rsconnect_args<<__GHA_EOF__\n--title\nMy App\n__GHA_EOF__" in written - - -def test_resolve_rsconnect_args_empty_uses_plain_form(tmp_path, monkeypatch): - output_file = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) - monkeypatch.setenv("INPUT_RSCONNECT_ARGS", "") - - assert main(["resolve-rsconnect-args"]) == 0 - - assert "rsconnect_args=\n" in output_file.read_text() - - -def test_resolve_rsconnect_args_unbalanced_quote_exits_nonzero(tmp_path, monkeypatch, capsys): - monkeypatch.setenv("GITHUB_OUTPUT", str(tmp_path / "github_output")) - monkeypatch.setenv("INPUT_RSCONNECT_ARGS", '--title "My App') - - assert main(["resolve-rsconnect-args"]) == 1 - assert "Error: Could not parse rsconnect-args" in capsys.readouterr().err diff --git a/tests/test_rsconnect_args.py b/tests/test_rsconnect_args.py deleted file mode 100644 index 4923a91..0000000 --- a/tests/test_rsconnect_args.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Tests for parsing the rsconnect-args input with shell quoting rules.""" - -from __future__ import annotations - -import pytest - -from connect_actions.rsconnect_args import RsconnectArgsError, parse_rsconnect_args - - -def test_empty_string_yields_no_args(): - assert parse_rsconnect_args("") == [] - - -def test_simple_space_separated_args_unchanged(): - # Backward compatibility: plain flags with no quoting behave exactly as - # the old unquoted Bash word-splitting did. - assert parse_rsconnect_args("--verbose --new") == ["--verbose", "--new"] - - -def test_quoted_value_with_spaces_stays_together(): - assert parse_rsconnect_args('--title "My App"') == ["--title", "My App"] - - -def test_single_quoted_value_with_spaces_stays_together(): - assert parse_rsconnect_args("--title 'My App'") == ["--title", "My App"] - - -def test_mixed_quoted_and_unquoted_args(): - assert parse_rsconnect_args('--verbose --title "My App" --new') == [ - "--verbose", - "--title", - "My App", - "--new", - ] - - -def test_extra_whitespace_between_args_is_ignored(): - assert parse_rsconnect_args(" --verbose --new ") == ["--verbose", "--new"] - - -def test_unbalanced_quote_raises_rsconnect_args_error(): - with pytest.raises(RsconnectArgsError, match="Could not parse rsconnect-args"): - parse_rsconnect_args('--title "My App') - - -def test_single_argument_value_with_equals_and_spaces(): - assert parse_rsconnect_args('--title="My App"') == ["--title=My App"]