From 72a9e0dbb4345fe5a7f6339b41865e3d268dbd8e Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Thu, 29 Jan 2026 09:04:51 -0500 Subject: [PATCH 1/5] feat: add start-only mode to keep Connect running When no command is provided after `--`, outputs JSON credentials (api_key, server, container_id) and keeps the container running. Adds `--stop` flag to stop a running container by ID. For GitHub Actions, adds outputs (api-key, server, container-id) and a stop input, enabling multi-step workflows. Closes #9 Co-Authored-By: Claude Opus 4.5 --- action.yml | 39 ++++++++++++++++++++++++++++++++++++--- main.py | 30 +++++++++++++++++++++++++++++- test_main.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index 0cd0ebb..3b584e5 100644 --- a/action.yml +++ b/action.yml @@ -26,8 +26,22 @@ inputs: description: 'Environment variables to pass to Docker container (one per line, format: KEY=VALUE)' required: false command: - description: 'Command to run against Connect' - required: true + description: 'Command to run against Connect (omit to start Connect and output credentials)' + required: false + stop: + description: 'Container ID to stop (use instead of starting a new container)' + required: false + +outputs: + api-key: + description: 'Connect API key (only set when no command is provided)' + value: ${{ steps.run.outputs.api-key }} + server: + description: 'Connect server URL (only set when no command is provided)' + value: ${{ steps.run.outputs.server }} + container-id: + description: 'Docker container ID (only set when no command is provided)' + value: ${{ steps.run.outputs.container-id }} runs: using: composite @@ -44,8 +58,16 @@ runs: run: echo "${{ inputs.license }}" > rstudio-connect.lic - name: Run command + id: run shell: bash run: | + # Handle stop mode + if [ -n "${{ inputs.stop }}" ]; then + with-connect --stop "${{ inputs.stop }}" + exit 0 + fi + + # Build arguments ARGS="--version ${{ inputs.version }} --port ${{ inputs.port }}" if [ -n "${{ inputs.config-file }}" ]; then ARGS="$ARGS --config ${{ inputs.config-file }}" @@ -63,6 +85,17 @@ runs: fi done <<< "${{ inputs.env }}" fi - with-connect $ARGS -- bash <<'SCRIPT' + + # Check if command is provided + if [ -z "${{ inputs.command }}" ]; then + # Start-only mode: capture JSON output and set outputs + OUTPUT=$(with-connect $ARGS) + echo "api-key=$(echo "$OUTPUT" | jq -r '.api_key')" >> $GITHUB_OUTPUT + echo "server=$(echo "$OUTPUT" | jq -r '.server')" >> $GITHUB_OUTPUT + echo "container-id=$(echo "$OUTPUT" | jq -r '.container_id')" >> $GITHUB_OUTPUT + else + # Run with command + with-connect $ARGS -- bash <<'SCRIPT' ${{ inputs.command }} SCRIPT + fi diff --git a/main.py b/main.py index 4b357b1..5c613d6 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import argparse import base64 +import json import os import re import socket @@ -61,6 +62,11 @@ def parse_args(): default=3939, help="Port to map the Connect container to (default: 3939)", ) + parser.add_argument( + "--stop", + metavar="CONTAINER_ID", + help="Stop a running Connect container by ID", + ) # Handle -- separator and capture remaining args if "--" in sys.argv: @@ -216,6 +222,17 @@ def main() -> int: """ args = parse_args() + # Handle --stop mode: just stop the container and exit + if args.stop: + client = docker.from_env() + try: + container = client.containers.get(args.stop) + container.stop() + print(f"Stopped container {args.stop}") + return 0 + except docker.errors.NotFound: + raise RuntimeError(f"Container not found: {args.stop}") + license_path = os.path.abspath(os.path.expanduser(args.license)) if not os.path.exists(license_path): raise RuntimeError(f"License file does not exist: {license_path}") @@ -286,6 +303,7 @@ def main() -> int: ) server_url = f"http://localhost:{args.port}" + stop_container = True try: print(f"Waiting for port {args.port} to open...") @@ -317,10 +335,20 @@ def main() -> int: exit_code = result.returncode except subprocess.CalledProcessError as e: exit_code = e.returncode + else: + # Start-only mode: output credentials and keep container running + output = { + "api_key": api_key, + "server": server_url, + "container_id": container.id, + } + print(json.dumps(output)) + stop_container = False return exit_code finally: - container.stop() + if stop_container: + container.stop() def is_port_open(host: str, port: int, timeout: float = 30.0) -> bool: diff --git a/test_main.py b/test_main.py index d77cfaf..4803915 100644 --- a/test_main.py +++ b/test_main.py @@ -226,6 +226,30 @@ def test_image_with_tag(): assert used_default is False +def test_stop_argument_in_help(): + """Test that --stop argument is available.""" + result = subprocess.run( + [sys.executable, "main.py", "--help"], + capture_output=True, + text=True, + ) + + assert "--stop" in result.stdout + assert "CONTAINER_ID" in result.stdout + + +def test_stop_nonexistent_container(): + """Test that --stop with nonexistent container returns error.""" + result = subprocess.run( + [sys.executable, "main.py", "--stop", "nonexistent_container_id"], + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "Container not found" in result.stderr + + if __name__ == "__main__": test_license_file_not_exists() print("✓ test_license_file_not_exists passed") @@ -296,4 +320,10 @@ def test_image_with_tag(): test_custom_port() print("✓ test_custom_port passed") + test_stop_argument_in_help() + print("✓ test_stop_argument_in_help passed") + + test_stop_nonexistent_container() + print("✓ test_stop_nonexistent_container passed") + print("\nAll tests passed!") From b2190b0b84deaca1a9876fffc47d510c03da441b Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Thu, 29 Jan 2026 11:14:34 -0500 Subject: [PATCH 2/5] docs: add CI test and documentation for start-only mode - Add test-action-start-only CI job that verifies outputs and stop - Document --stop CLI option - Document start-only mode for CLI - Update GitHub Action inputs table (command now optional, add stop) - Add GitHub Action outputs table - Add multi-step workflow example Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++ README.md | 71 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa1a9e2..138cbfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,60 @@ jobs: [ "$TEST_STRING" = "This contains single quotes" ] || exit 1 echo "✓ Multiline test passed - variables, single quotes, and double quotes all work" + test-action-start-only: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Start Connect (no command) + id: start-connect + uses: ./ + with: + version: 2024.08.0 + license: ${{ secrets.CONNECT_LICENSE }} + + - name: Verify outputs are set + run: | + set -euo pipefail + echo "API Key length: ${#API_KEY}" + echo "Server: $SERVER" + echo "Container ID: $CONTAINER_ID" + [ -n "$API_KEY" ] || { echo "ERROR: api-key output is empty"; exit 1; } + [ -n "$SERVER" ] || { echo "ERROR: server output is empty"; exit 1; } + [ -n "$CONTAINER_ID" ] || { echo "ERROR: container-id output is empty"; exit 1; } + echo "✓ All outputs are set" + env: + API_KEY: ${{ steps.start-connect.outputs.api-key }} + SERVER: ${{ steps.start-connect.outputs.server }} + CONTAINER_ID: ${{ steps.start-connect.outputs.container-id }} + + - name: Use Connect with outputs + run: | + set -euo pipefail + RESPONSE=$(curl -f -H "Authorization: Key $API_KEY" "$SERVER/__api__/v1/content") + echo "Response: $RESPONSE" + [ "$RESPONSE" = "[]" ] || exit 1 + echo "✓ Successfully used Connect with action outputs" + env: + API_KEY: ${{ steps.start-connect.outputs.api-key }} + SERVER: ${{ steps.start-connect.outputs.server }} + + - name: Stop Connect + uses: ./ + with: + stop: ${{ steps.start-connect.outputs.container-id }} + + - name: Verify container stopped + run: | + set -euo pipefail + if docker ps -q --filter "id=$CONTAINER_ID" | grep -q .; then + echo "ERROR: Container is still running" + exit 1 + fi + echo "✓ Container successfully stopped" + env: + CONTAINER_ID: ${{ steps.start-connect.outputs.container-id }} + test-cli: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 8852270..1fb1edb 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Without `bash -c`, the environment variables would be evaluated before `with-con | `--config` | | Path to optional rstudio-connect.gcfg configuration file | | `--port` | `3939` | Port to map the Connect container to. Allows running multiple Connect instances simultaneously. | | `-e`, `--env` | | Environment variables to pass to the Docker container (format: KEY=VALUE). Can be specified multiple times. | +| `--stop` | | Stop a running Connect container by ID (use instead of starting a new container). | Example: @@ -86,6 +87,23 @@ You can use this to override Connect server configuration by passing in `CONNECT If you need env vars that are useful for the command running after `--`, just set them in the environment from which you call `with-connect`: the command will inherit that environment. +### Start-Only Mode + +If you omit the command after `--`, Connect will start and remain running. The tool outputs JSON with credentials you can use to interact with Connect: + +```bash +with-connect --license ./rstudio-connect.lic +# Outputs: {"api_key": "...", "server": "http://localhost:3939", "container_id": "..."} +``` + +You can then use the container ID to stop Connect when you're done: + +```bash +with-connect --stop +``` + +This is useful when you need to run multiple commands or use other tools against the running Connect instance. + ## GitHub Actions This project contains a GitHub Action for use in CI/CD workflows. Use the `@main` tag to reference the action. @@ -105,7 +123,18 @@ The GitHub Action supports the following inputs: | `port` | No | `3939` | Port to map the Connect container to | | `quiet` | No | `false` | Suppress progress indicators during image pull | | `env` | No | | Environment variables to pass to Docker container (one per line, format: KEY=VALUE) | -| `command` | Yes | | Command to run against Connect | +| `command` | No | | Command to run against Connect (omit for start-only mode) | +| `stop` | No | | Container ID to stop (use instead of starting a new container) | + +### GitHub Action Outputs + +When no `command` is provided (start-only mode), the action sets these outputs: + +| Output | Description | +|----------------|------------------------------------------| +| `api-key` | Connect API key for authentication | +| `server` | Connect server URL (e.g., `http://localhost:3939`) | +| `container-id` | Docker container ID (use with `stop` input to stop the container) | ### Deploy a Connect Manifest @@ -184,6 +213,46 @@ The `$CONNECT_API_KEY` and `$CONNECT_SERVER` environment variables are available command: rsconnect deploy manifest . ``` +### Multi-Step Workflows (Start-Only Mode) + +For workflows that need to run multiple steps against Connect, or use other actions with the running instance, use start-only mode: + +```yaml +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + # Start Connect without a command - it will keep running + - name: Start Connect + id: connect + uses: posit-dev/with-connect@main + with: + version: 2025.09.0 + license: ${{ secrets.CONNECT_LICENSE_FILE }} + + # Use the outputs in subsequent steps + - name: Deploy content + run: rsconnect deploy manifest . + env: + CONNECT_API_KEY: ${{ steps.connect.outputs.api-key }} + CONNECT_SERVER: ${{ steps.connect.outputs.server }} + + # Use another action with Connect + - name: Run integration tests + uses: some-other-action@v1 + with: + connect-url: ${{ steps.connect.outputs.server }} + api-key: ${{ steps.connect.outputs.api-key }} + + # Stop Connect when done + - name: Stop Connect + uses: posit-dev/with-connect@main + with: + stop: ${{ steps.connect.outputs.container-id }} +``` + ## Minimum Version Posit Connect 2022.10.0 or later is required. Earlier versions did not have the bootstrap endpoint used in this utility. From d256074abc319f9b69de2dbf83d0289c062145ac Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Thu, 29 Jan 2026 11:31:22 -0500 Subject: [PATCH 3/5] fix: send progress messages to stderr Progress and diagnostic messages now go to stderr, leaving stdout clean for machine-readable output (JSON in start-only mode). This follows Unix conventions where stderr is used for diagnostic output, similar to curl, wget, git, etc. Co-Authored-By: Claude Opus 4.5 --- main.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/main.py b/main.py index 5c613d6..dfabfa5 100644 --- a/main.py +++ b/main.py @@ -105,9 +105,9 @@ def pull_image(client, base_image: str, tag: str, quiet: bool) -> None: image_name = f"{base_image}:{tag}" if quiet: - print(f"Pulling image {image_name}...") + print(f"Pulling image {image_name}...", file=sys.stderr) else: - print(f"Pulling image {image_name}...", end="", flush=True) + print(f"Pulling image {image_name}...", end="", flush=True, file=sys.stderr) pull_stream = client.api.pull( base_image, tag=tag, platform="linux/amd64", stream=True, decode=True @@ -118,12 +118,12 @@ def pull_image(client, base_image: str, tag: str, quiet: bool) -> None: if "status" in chunk: dot_count += 1 if dot_count % 10 == 0 and not quiet: - print(".", end="", flush=True) + print(".", end="", flush=True, file=sys.stderr) if not quiet: - print() + print(file=sys.stderr) - print(f"Successfully pulled {image_name}") + print(f"Successfully pulled {image_name}", file=sys.stderr) def ensure_image(client, base_image: str, tag: str, version: str, quiet: bool) -> None: @@ -140,14 +140,14 @@ def ensure_image(client, base_image: str, tag: str, version: str, quiet: bool) - is_release = version in ("latest", "release", "preview") if not is_release and has_local_image(client, image_name): - print(f"Using locally cached image {image_name}") + print(f"Using locally cached image {image_name}", file=sys.stderr) return try: pull_image(client, base_image, tag, quiet) except Exception as e: if has_local_image(client, image_name): - print(f"Pull failed, but using locally cached image {image_name}") + print(f"Pull failed, but using locally cached image {image_name}", file=sys.stderr) else: raise RuntimeError(f"Failed to pull image and no local copy available: {e}") @@ -228,7 +228,7 @@ def main() -> int: try: container = client.containers.get(args.stop) container.stop() - print(f"Stopped container {args.stop}") + print(f"Stopped container {args.stop}", file=sys.stderr) return 0 except docker.errors.NotFound: raise RuntimeError(f"Container not found: {args.stop}") @@ -250,7 +250,7 @@ def main() -> int: if args.image: base_image, tag, used_default = parse_image_spec(args.image) if used_default: - print(f"No tag specified for image '{args.image}'. Using default tag 'latest'.") + print(f"No tag specified for image '{args.image}'. Using default tag 'latest'.", file=sys.stderr) else: base_image, tag = get_docker_tag(args.version) image_name = f"{base_image}:{tag}" @@ -306,16 +306,16 @@ def main() -> int: stop_container = True try: - print(f"Waiting for port {args.port} to open...") + print(f"Waiting for port {args.port} to open...", file=sys.stderr) if not is_port_open("localhost", args.port, timeout=60.0): - print("\nContainer logs:") - print(container.logs().decode("utf-8", errors="replace")) + print("\nContainer logs:", file=sys.stderr) + print(container.logs().decode("utf-8", errors="replace"), file=sys.stderr) raise RuntimeError("Posit Connect did not start within 60 seconds.") - print("Waiting for HTTP server to start...") + print("Waiting for HTTP server to start...", file=sys.stderr) if not wait_for_http_server(container, timeout=60.0, poll_interval=2.0): - print("\nContainer logs:") - print(container.logs().decode("utf-8", errors="replace")) + print("\nContainer logs:", file=sys.stderr) + print(container.logs().decode("utf-8", errors="replace"), file=sys.stderr) raise RuntimeError( "Posit Connect did not log HTTP server start within 60 seconds." ) @@ -402,11 +402,11 @@ def wait_for_http_server( if not version: version = extract_server_version(logs) if version: - print(f"Running Posit Connect v{version}") + print(f"Running Posit Connect v{version}", file=sys.stderr) if "Unable to obtain a valid license" in logs: - print("\nContainer logs:") - print(logs) + print("\nContainer logs:", file=sys.stderr) + print(logs, file=sys.stderr) container.stop() raise RuntimeError( "Unable to obtain a valid license. Your Posit Connect license may be expired or invalid. Please check your license file." @@ -444,17 +444,17 @@ def get_api_key(bootstrap_secret: str, container, server_url: str) -> str: if response and "api_key" in response: api_key = response["api_key"] if not api_key: - print("\nContainer logs:") - print(container.logs().decode("utf-8", errors="replace")) + print("\nContainer logs:", file=sys.stderr) + print(container.logs().decode("utf-8", errors="replace"), file=sys.stderr) raise RuntimeError("Bootstrap succeeded but returned empty API key") return api_key else: - print("\nContainer logs:") - print(container.logs().decode("utf-8", errors="replace")) + print("\nContainer logs:", file=sys.stderr) + print(container.logs().decode("utf-8", errors="replace"), file=sys.stderr) raise RuntimeError(f"Bootstrap returned unexpected response: {response}") except Exception as e: - print("\nContainer logs:") - print(container.logs().decode("utf-8", errors="replace")) + print("\nContainer logs:", file=sys.stderr) + print(container.logs().decode("utf-8", errors="replace"), file=sys.stderr) raise RuntimeError(f"Failed to bootstrap Connect and retrieve API key: {e}") From 43dad2b57f37e28505ab9a38219a35bafa1f5e2b Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Tue, 3 Feb 2026 15:35:33 -0500 Subject: [PATCH 4/5] refactor: use shell format output and improve --stop UX - Output shell variables instead of JSON (CONNECT_API_KEY, CONNECT_SERVER, CONTAINER_ID) - --stop without argument now uses CONTAINER_ID env var - Update action.yml to parse shell format (no jq dependency) - Update README with eval example Co-Authored-By: Claude Opus 4.5 --- README.md | 17 ++++++++++++----- action.yml | 8 ++++---- main.py | 26 ++++++++++++++------------ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1fb1edb..c78bb0d 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Without `bash -c`, the environment variables would be evaluated before `with-con | `--config` | | Path to optional rstudio-connect.gcfg configuration file | | `--port` | `3939` | Port to map the Connect container to. Allows running multiple Connect instances simultaneously. | | `-e`, `--env` | | Environment variables to pass to the Docker container (format: KEY=VALUE). Can be specified multiple times. | -| `--stop` | | Stop a running Connect container by ID (use instead of starting a new container). | +| `--stop` | | Stop a running Connect container by ID, or use `CONTAINER_ID` env var if not specified. | Example: @@ -89,17 +89,24 @@ If you need env vars that are useful for the command running after `--`, just se ### Start-Only Mode -If you omit the command after `--`, Connect will start and remain running. The tool outputs JSON with credentials you can use to interact with Connect: +If you omit the command after `--`, Connect will start and remain running. The tool outputs shell variables you can use to interact with Connect: ```bash with-connect --license ./rstudio-connect.lic -# Outputs: {"api_key": "...", "server": "http://localhost:3939", "container_id": "..."} +# Outputs: +# CONNECT_API_KEY=... +# CONNECT_SERVER=http://localhost:3939 +# CONTAINER_ID=... ``` -You can then use the container ID to stop Connect when you're done: +You can eval the output to set the variables in your shell: ```bash -with-connect --stop +eval $(with-connect --license ./rstudio-connect.lic) +curl -H "Authorization: Key $CONNECT_API_KEY" $CONNECT_SERVER/__api__/v1/content + +# Stop Connect when done (--stop without argument uses $CONTAINER_ID) +with-connect --stop ``` This is useful when you need to run multiple commands or use other tools against the running Connect instance. diff --git a/action.yml b/action.yml index 3b584e5..9eab322 100644 --- a/action.yml +++ b/action.yml @@ -88,11 +88,11 @@ runs: # Check if command is provided if [ -z "${{ inputs.command }}" ]; then - # Start-only mode: capture JSON output and set outputs + # Start-only mode: capture shell output and set outputs OUTPUT=$(with-connect $ARGS) - echo "api-key=$(echo "$OUTPUT" | jq -r '.api_key')" >> $GITHUB_OUTPUT - echo "server=$(echo "$OUTPUT" | jq -r '.server')" >> $GITHUB_OUTPUT - echo "container-id=$(echo "$OUTPUT" | jq -r '.container_id')" >> $GITHUB_OUTPUT + echo "api-key=$(echo "$OUTPUT" | grep '^CONNECT_API_KEY=' | cut -d= -f2-)" >> $GITHUB_OUTPUT + echo "server=$(echo "$OUTPUT" | grep '^CONNECT_SERVER=' | cut -d= -f2-)" >> $GITHUB_OUTPUT + echo "container-id=$(echo "$OUTPUT" | grep '^CONTAINER_ID=' | cut -d= -f2-)" >> $GITHUB_OUTPUT else # Run with command with-connect $ARGS -- bash <<'SCRIPT' diff --git a/main.py b/main.py index dfabfa5..54e9b69 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,5 @@ import argparse import base64 -import json import os import re import socket @@ -64,8 +63,11 @@ def parse_args(): ) parser.add_argument( "--stop", + nargs="?", + default=None, + const="", # sentinel for --stop without argument metavar="CONTAINER_ID", - help="Stop a running Connect container by ID", + help="Stop a running Connect container by ID (uses CONTAINER_ID env var if not specified)", ) # Handle -- separator and capture remaining args @@ -223,15 +225,18 @@ def main() -> int: args = parse_args() # Handle --stop mode: just stop the container and exit - if args.stop: + if args.stop is not None: + container_id = args.stop or os.environ.get("CONTAINER_ID") + if not container_id: + raise RuntimeError("No container ID provided and CONTAINER_ID environment variable not set") client = docker.from_env() try: - container = client.containers.get(args.stop) + container = client.containers.get(container_id) container.stop() - print(f"Stopped container {args.stop}", file=sys.stderr) + print(f"Stopped container {container_id}", file=sys.stderr) return 0 except docker.errors.NotFound: - raise RuntimeError(f"Container not found: {args.stop}") + raise RuntimeError(f"Container not found: {container_id}") license_path = os.path.abspath(os.path.expanduser(args.license)) if not os.path.exists(license_path): @@ -337,12 +342,9 @@ def main() -> int: exit_code = e.returncode else: # Start-only mode: output credentials and keep container running - output = { - "api_key": api_key, - "server": server_url, - "container_id": container.id, - } - print(json.dumps(output)) + print(f"CONNECT_API_KEY={api_key}") + print(f"CONNECT_SERVER={server_url}") + print(f"CONTAINER_ID={container.id}") stop_container = False return exit_code From 8ed88b3a77703a3cea11e4a7263e77724b411216 Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Tue, 3 Feb 2026 15:39:29 -0500 Subject: [PATCH 5/5] refactor: use same names for outputs as env vars Output names now match env var names (CONNECT_API_KEY, CONNECT_SERVER, CONTAINER_ID), allowing direct piping to GITHUB_OUTPUT without parsing. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 26 +++++++++++++------------- README.md | 20 ++++++++++---------- action.yml | 19 ++++++++----------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 138cbfb..4407495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,33 +71,33 @@ jobs: - name: Verify outputs are set run: | set -euo pipefail - echo "API Key length: ${#API_KEY}" - echo "Server: $SERVER" + echo "API Key length: ${#CONNECT_API_KEY}" + echo "Server: $CONNECT_SERVER" echo "Container ID: $CONTAINER_ID" - [ -n "$API_KEY" ] || { echo "ERROR: api-key output is empty"; exit 1; } - [ -n "$SERVER" ] || { echo "ERROR: server output is empty"; exit 1; } - [ -n "$CONTAINER_ID" ] || { echo "ERROR: container-id output is empty"; exit 1; } + [ -n "$CONNECT_API_KEY" ] || { echo "ERROR: CONNECT_API_KEY output is empty"; exit 1; } + [ -n "$CONNECT_SERVER" ] || { echo "ERROR: CONNECT_SERVER output is empty"; exit 1; } + [ -n "$CONTAINER_ID" ] || { echo "ERROR: CONTAINER_ID output is empty"; exit 1; } echo "✓ All outputs are set" env: - API_KEY: ${{ steps.start-connect.outputs.api-key }} - SERVER: ${{ steps.start-connect.outputs.server }} - CONTAINER_ID: ${{ steps.start-connect.outputs.container-id }} + CONNECT_API_KEY: ${{ steps.start-connect.outputs.CONNECT_API_KEY }} + CONNECT_SERVER: ${{ steps.start-connect.outputs.CONNECT_SERVER }} + CONTAINER_ID: ${{ steps.start-connect.outputs.CONTAINER_ID }} - name: Use Connect with outputs run: | set -euo pipefail - RESPONSE=$(curl -f -H "Authorization: Key $API_KEY" "$SERVER/__api__/v1/content") + RESPONSE=$(curl -f -H "Authorization: Key $CONNECT_API_KEY" "$CONNECT_SERVER/__api__/v1/content") echo "Response: $RESPONSE" [ "$RESPONSE" = "[]" ] || exit 1 echo "✓ Successfully used Connect with action outputs" env: - API_KEY: ${{ steps.start-connect.outputs.api-key }} - SERVER: ${{ steps.start-connect.outputs.server }} + CONNECT_API_KEY: ${{ steps.start-connect.outputs.CONNECT_API_KEY }} + CONNECT_SERVER: ${{ steps.start-connect.outputs.CONNECT_SERVER }} - name: Stop Connect uses: ./ with: - stop: ${{ steps.start-connect.outputs.container-id }} + stop: ${{ steps.start-connect.outputs.CONTAINER_ID }} - name: Verify container stopped run: | @@ -108,7 +108,7 @@ jobs: fi echo "✓ Container successfully stopped" env: - CONTAINER_ID: ${{ steps.start-connect.outputs.container-id }} + CONTAINER_ID: ${{ steps.start-connect.outputs.CONTAINER_ID }} test-cli: runs-on: ubuntu-latest diff --git a/README.md b/README.md index c78bb0d..9243311 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,11 @@ The GitHub Action supports the following inputs: When no `command` is provided (start-only mode), the action sets these outputs: -| Output | Description | -|----------------|------------------------------------------| -| `api-key` | Connect API key for authentication | -| `server` | Connect server URL (e.g., `http://localhost:3939`) | -| `container-id` | Docker container ID (use with `stop` input to stop the container) | +| Output | Description | +|-------------------|------------------------------------------| +| `CONNECT_API_KEY` | Connect API key for authentication | +| `CONNECT_SERVER` | Connect server URL (e.g., `http://localhost:3939`) | +| `CONTAINER_ID` | Docker container ID (use with `stop` input to stop the container) | ### Deploy a Connect Manifest @@ -243,21 +243,21 @@ jobs: - name: Deploy content run: rsconnect deploy manifest . env: - CONNECT_API_KEY: ${{ steps.connect.outputs.api-key }} - CONNECT_SERVER: ${{ steps.connect.outputs.server }} + CONNECT_API_KEY: ${{ steps.connect.outputs.CONNECT_API_KEY }} + CONNECT_SERVER: ${{ steps.connect.outputs.CONNECT_SERVER }} # Use another action with Connect - name: Run integration tests uses: some-other-action@v1 with: - connect-url: ${{ steps.connect.outputs.server }} - api-key: ${{ steps.connect.outputs.api-key }} + connect-url: ${{ steps.connect.outputs.CONNECT_SERVER }} + api-key: ${{ steps.connect.outputs.CONNECT_API_KEY }} # Stop Connect when done - name: Stop Connect uses: posit-dev/with-connect@main with: - stop: ${{ steps.connect.outputs.container-id }} + stop: ${{ steps.connect.outputs.CONTAINER_ID }} ``` ## Minimum Version diff --git a/action.yml b/action.yml index 9eab322..d23f0e1 100644 --- a/action.yml +++ b/action.yml @@ -33,15 +33,15 @@ inputs: required: false outputs: - api-key: + CONNECT_API_KEY: description: 'Connect API key (only set when no command is provided)' - value: ${{ steps.run.outputs.api-key }} - server: + value: ${{ steps.run.outputs.CONNECT_API_KEY }} + CONNECT_SERVER: description: 'Connect server URL (only set when no command is provided)' - value: ${{ steps.run.outputs.server }} - container-id: + value: ${{ steps.run.outputs.CONNECT_SERVER }} + CONTAINER_ID: description: 'Docker container ID (only set when no command is provided)' - value: ${{ steps.run.outputs.container-id }} + value: ${{ steps.run.outputs.CONTAINER_ID }} runs: using: composite @@ -88,11 +88,8 @@ runs: # Check if command is provided if [ -z "${{ inputs.command }}" ]; then - # Start-only mode: capture shell output and set outputs - OUTPUT=$(with-connect $ARGS) - echo "api-key=$(echo "$OUTPUT" | grep '^CONNECT_API_KEY=' | cut -d= -f2-)" >> $GITHUB_OUTPUT - echo "server=$(echo "$OUTPUT" | grep '^CONNECT_SERVER=' | cut -d= -f2-)" >> $GITHUB_OUTPUT - echo "container-id=$(echo "$OUTPUT" | grep '^CONTAINER_ID=' | cut -d= -f2-)" >> $GITHUB_OUTPUT + # Start-only mode: output is already in KEY=value format + with-connect $ARGS >> $GITHUB_OUTPUT else # Run with command with-connect $ARGS -- bash <<'SCRIPT'