Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
id: start-connect
uses: ./
with:
version: 2024.08.0
version: 2024.08.0 # legacy rstudio/rstudio-connect image
license: ${{ secrets.CONNECT_LICENSE }}

- name: Verify outputs are set
Expand Down Expand Up @@ -112,6 +112,113 @@ jobs:
env:
CONTAINER_ID: ${{ steps.start-connect.outputs.CONTAINER_ID }}

# Full --reset cycle (dirty -> reset -> pristine + same key).
test-action-reset:
name: test-action-reset (${{ matrix.label }})
strategy:
fail-fast: false
matrix:
include:
- version: "release" # modern ghcr.io/posit-dev/connect image
label: modern
- version: "2024.08.0" # legacy rstudio/rstudio-connect image
label: legacy
- version: "2022.10.0" # README's documented minimum (bionic image)
label: legacy-minimum
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Start Connect (start-only)
id: start
uses: ./
with:
version: ${{ matrix.version }}
license: ${{ secrets.CONNECT_LICENSE }}

- name: Deploy content to dirty the instance
run: |
set -euo pipefail
curl -f -s -X POST "$CONNECT_SERVER/__api__/v1/content" \
-H "Authorization: Key $CONNECT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"ci-reset-test","title":"CI Reset Test"}' > /dev/null
COUNT=$(curl -f -s -H "Authorization: Key $CONNECT_API_KEY" "$CONNECT_SERVER/__api__/v1/content" \
| python3 -c 'import sys, json; print(len(json.load(sys.stdin)))')
echo "Content count after deploy: $COUNT"
[ "$COUNT" = "1" ] || { echo "ERROR: expected 1 content item"; exit 1; }
echo "✓ Instance dirtied with one content item"
env:
CONNECT_API_KEY: ${{ steps.start.outputs.CONNECT_API_KEY }}
CONNECT_SERVER: ${{ steps.start.outputs.CONNECT_SERVER }}

- name: Reset Connect
uses: ./
with:
reset: ${{ steps.start.outputs.CONTAINER_ID }}

- name: Verify pristine state with the same API key
run: |
set -euo pipefail
RESPONSE=$(curl -f -H "Authorization: Key $CONNECT_API_KEY" "$CONNECT_SERVER/__api__/v1/content")
echo "Content after reset: $RESPONSE"
[ "$RESPONSE" = "[]" ] || { echo "ERROR: content should be wiped after reset"; exit 1; }
if ! docker ps -q --filter "id=$CONTAINER_ID" | grep -q .; then
echo "ERROR: container should still be running after reset"
exit 1
fi
echo "✓ Reset produced a clean Connect, kept the same API key, and left the container running"
env:
CONNECT_API_KEY: ${{ steps.start.outputs.CONNECT_API_KEY }}
CONNECT_SERVER: ${{ steps.start.outputs.CONNECT_SERVER }}
CONTAINER_ID: ${{ steps.start.outputs.CONTAINER_ID }}

- name: Stop Connect
uses: ./
with:
stop: ${{ steps.start.outputs.CONTAINER_ID }}

# --reset must refuse (not silently no-op) when Connect points at an external
# database, against a live Postgres-backed Connect rather than a mock.
test-reset-external-db:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Create license file
run: echo "${{ secrets.CONNECT_LICENSE }}" > rstudio-connect.lic

- name: Run external-database reset test
run: |
CONNECT_LICENSE_FILE=rstudio-connect.lic \
uv run --with pytest pytest test_integration.py -k test_reset_refuses_external_database -v

# --reset must work against a custom Server.DataDir path that doesn't already
# exist in the image (Connect logs "Creating", not "Using", the first time).
test-reset-custom-datadir:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Create license file
run: echo "${{ secrets.CONNECT_LICENSE }}" > rstudio-connect.lic

- name: Run custom-DataDir reset test
run: |
CONNECT_LICENSE_FILE=rstudio-connect.lic \
uv run --with pytest pytest test_integration.py -k test_reset_works_with_custom_datadir -v

# CLI command mode on the legacy rstudio/rstudio-connect image.
test-cli:
runs-on: ubuntu-latest
steps:
Expand Down
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Without `bash -c`, the environment variables would be evaluated before `with-con
| `--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, or use `CONTAINER_ID` env var if not specified. |
| `--reset` | | Reset a running start-only container to its clean baseline (same container, port, and API key), or use `CONTAINER_ID` env var if not specified. |

Example:

Expand Down Expand Up @@ -105,12 +106,50 @@ You can eval the output to set the variables in your shell:
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
# Stop Connect when done
with-connect --stop "$CONTAINER_ID"
```

`eval` sets these as ordinary shell variables, so pass `"$CONTAINER_ID"` explicitly. The no-argument forms of `--stop`/`--reset` instead read a `CONTAINER_ID` environment variable, which is how the GitHub Action wires them up.

This is useful when you need to run multiple commands or use other tools against the running Connect instance.

### Resetting Connect

Every start-only container can be reset. `with-connect --reset` returns Connect to its clean, just-bootstrapped state — no deployed content, no extra users — **without stopping the container**. The same `CONNECT_API_KEY`, `CONNECT_SERVER`, and `CONTAINER_ID` stay valid, so a test framework can reset between runs and keep the credentials it already holds:

```bash
eval $(with-connect --license ./rstudio-connect.lic)

# ... run a test that deploys content ...

# Reset to a clean Connect between tests
with-connect --reset "$CONTAINER_ID"
# The same $CONNECT_API_KEY and $CONNECT_SERVER still work; Connect is now clean.

# ... run the next test ...

with-connect --stop "$CONTAINER_ID"
```

Reset is fast (usually a few seconds) because it never restarts the container, re-pulls the image, or re-bootstraps. Under the hood, start-only containers run Connect under a keep-alive process so Connect can be cycled in place; the reset restores a snapshot of the data directory captured right after bootstrap. Reset only applies to start-only containers (command mode containers are ephemeral).

`--reset` resolves Connect's effective data directory at runtime (from its startup log), so it works with the default location (`/var/lib/rstudio-connect`), a custom `Server.DataDir` (via `--config` or `CONNECT_SERVER_DATADIR`), and older images that default elsewhere (e.g. `/data`). It restores Connect's built-in SQLite database, so if Connect is pointed at an external database `--reset` refuses to run and reports an error rather than silently leaving that state in place.

#### Detecting a crashed Connect

Because a start-only container stays running under the keep-alive process, a crashed Connect does **not** stop the container. To make crashes visible, start-only containers are given a healthcheck that probes Connect's `/__ping__` endpoint. Check it during a test run:

```bash
docker inspect --format '{{.State.Health.Status}}' "$CONTAINER_ID"
# healthy -> Connect is serving
# unhealthy -> Connect stopped responding (e.g. crashed) and did not recover
```

Nothing auto-restarts Connect, so a crash stays `unhealthy` until the next `--reset`. Assert on `healthy` (rather than container liveness) if a test needs to confirm Connect stayed up.

> Note: the health status is refreshed on the container's healthcheck interval, so immediately after a `--reset` it may briefly lag (reset polls `/__ping__` directly and returns as soon as Connect is serving). For an immediate readiness signal use `/__ping__`; for "did Connect stay up over time" use the health status.

## GitHub Actions

This project contains a GitHub Action for use in CI/CD workflows. Use the `@main` tag to reference the action.
Expand All @@ -132,6 +171,7 @@ The GitHub Action supports the following inputs:
| `env` | No | | Environment variables to pass to Docker container (one per line, format: KEY=VALUE) |
| `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) |
| `reset` | No | | Container ID of a start-only container to reset to its clean baseline (use instead of starting a new container) |

### GitHub Action Outputs

Expand Down
13 changes: 13 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ inputs:
stop:
description: 'Container ID to stop (use instead of starting a new container)'
required: false
reset:
description: 'Container ID of a start-only container to reset to its clean baseline (use instead of starting a new container)'
required: false

outputs:
CONNECT_API_KEY:
Expand All @@ -53,7 +56,11 @@ runs:
shell: bash
run: uv tool install ${{ github.action_path }}

# Only write the license when starting a new container. On stop/reset there is
# no license input, and the running container bind-mounts this file as its
# license -- rewriting it here would blank that out and break --reset.
- name: Create license file
if: ${{ inputs.stop == '' && inputs.reset == '' }}
shell: bash
run: echo "${{ inputs.license }}" > rstudio-connect.lic

Expand All @@ -67,6 +74,12 @@ runs:
exit 0
fi

# Handle reset mode
if [ -n "${{ inputs.reset }}" ]; then
with-connect --reset "${{ inputs.reset }}"
exit 0
fi

# Build arguments
ARGS="--version ${{ inputs.version }} --port ${{ inputs.port }}"
if [ -n "${{ inputs.config-file }}" ]; then
Expand Down
Loading
Loading