-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(cli,ci): 'rocketride validate' subcommand (Python + TS) and validate-pipes GitHub Action #1573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nihalnihalani
wants to merge
5
commits into
rocketride-org:develop
Choose a base branch
from
nihalnihalani:feat/RR-1572-validate-cli-github-action
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c92e0d5
fix(cli): use the Node process global — namespace import breaks signa…
nihalnihalani 1675fc8
fix(ai): wrap rrext_validate payload in the {'pipeline': ...} envelop…
nihalnihalani 80f3e00
feat(cli): add 'rocketride validate' subcommand to the Python and Typ…
nihalnihalani d237562
feat(ci): add validate-pipes composite GitHub Action
nihalnihalani 32649c2
docs: document the validate CLI subcommand and CI usage
nihalnihalani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Validate RocketRide Pipelines | ||
|
|
||
| Composite GitHub Action that validates `.pipe` pipeline files against a real | ||
| RocketRide engine. It: | ||
|
|
||
| 1. Sets up Python and installs the [`rocketride`](https://pypi.org/project/rocketride/) CLI. | ||
| 2. Starts the RocketRide engine in Docker and waits for its public `/version` | ||
| endpoint to respond (available on every published engine image). | ||
| 3. Resolves the files to validate — on pull requests, only the `.pipe` files | ||
| changed relative to the base branch by default. | ||
| 4. Runs `rocketride validate` against the local engine. | ||
| 5. Always removes the engine container, even when validation fails. | ||
|
|
||
| The job fails when any pipeline is invalid (CLI exit code `1`) or when the | ||
| engine cannot be started or reached (exit code `2`). | ||
|
|
||
| ## Requirements | ||
|
|
||
| - A Linux runner with Docker available (e.g. `ubuntu-latest`). | ||
| - The repository checked out before this action runs (`actions/checkout`). | ||
| The default `fetch-depth: 1` is fine — the action fetches the PR base | ||
| commit itself when `changed-only` is enabled. | ||
| - A `rocketride` CLI release that includes the `validate` command. The | ||
| install step fails fast with a clear error when the installed release does | ||
| not; use the `cli-version` input to pin a release that does. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Default | Description | | ||
| | --- | --- | --- | | ||
| | `files` | `**/*.pipe` | Glob pattern(s) selecting the files to validate. Multiple patterns may be separated by whitespace; `**` recurses into subdirectories. | | ||
| | `changed-only` | `true` | On `pull_request` events, validate only the matching files changed vs the PR base (via `git diff`). Ignored on other events. | | ||
| | `engine-image` | `ghcr.io/rocketride-org/rocketride-engine:latest` | Docker image of the engine to validate against. | | ||
| | `engine-port` | `5565` | Host port mapped to the engine container's internal port `5565`. | | ||
| | `api-key` | _(empty)_ | Optional API key, passed to the CLI via the `ROCKETRIDE_APIKEY` environment variable (never on the command line). | | ||
| | `python-version` | `3.12` | Python version used to run the CLI. | | ||
| | `cli-version` | _(empty)_ | pip version specifier for the `rocketride` CLI (e.g. `==1.2.3` or `>=1.2.3`). Must resolve to a release that includes the `validate` subcommand. Installs the latest release when empty. | | ||
| | `fail-on-warnings` | `false` | Fail the job when a pipeline validates with warnings (the CLI itself exits `0` on warnings). | | ||
|
|
||
| ## Usage | ||
|
|
||
| ```yaml | ||
| name: Validate pipelines | ||
|
|
||
| on: | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| - uses: ./.github/actions/validate-pipes | ||
| with: | ||
| files: 'examples/**/*.pipe' | ||
| # api-key: ${{ secrets.ROCKETRIDE_APIKEY }} # if your engine requires auth | ||
| ``` | ||
|
|
||
| > [!TIP] | ||
| > For reproducible CI results, pin `engine-image` to a released version tag | ||
| > (e.g. `ghcr.io/rocketride-org/rocketride-engine:1.3.0`) or an image digest | ||
| > instead of `latest`, and bump it deliberately. | ||
|
|
||
| ## Running the same check locally | ||
|
|
||
| ```bash | ||
| docker run -d --name rocketride-engine -p 5565:5565 \ | ||
| ghcr.io/rocketride-org/rocketride-engine:latest | ||
| curl -fsS --retry 30 --retry-delay 2 --retry-all-errors http://127.0.0.1:5565/version | ||
|
|
||
| pip install rocketride | ||
| ROCKETRIDE_URI=http://127.0.0.1:5565 rocketride validate examples/*.pipe | ||
|
|
||
| docker rm -f rocketride-engine | ||
| ``` | ||
|
|
||
| `rocketride validate` exits `0` when all files are valid, `1` when at least | ||
| one file fails validation, and `2` on usage errors, unreadable files, or | ||
| connection failures. Add `--json` for a machine-readable report. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| name: 'Validate RocketRide Pipelines' | ||
| description: >- | ||
| Start a RocketRide engine in Docker and validate .pipe pipeline files with | ||
| the rocketride CLI. Fails the job when any pipeline is invalid. | ||
| author: 'RocketRide' | ||
|
|
||
| branding: | ||
| icon: 'check-circle' | ||
| color: 'orange' | ||
|
|
||
| inputs: | ||
| files: | ||
| description: >- | ||
| Glob pattern(s) selecting the .pipe files to validate. Multiple patterns | ||
| may be separated by whitespace. Patterns are expanded with bash globstar, | ||
| so '**' recurses into subdirectories. | ||
| required: false | ||
| default: '**/*.pipe' | ||
| changed-only: | ||
| description: >- | ||
| On pull_request events, validate only the matching .pipe files that | ||
| changed relative to the PR base (via git diff). Ignored on other events. | ||
| required: false | ||
| default: 'true' | ||
| engine-image: | ||
| description: 'Docker image of the RocketRide engine to validate against.' | ||
| required: false | ||
| default: 'ghcr.io/rocketride-org/rocketride-engine:latest' | ||
| engine-port: | ||
| description: "Host port mapped to the engine container's internal port 5565." | ||
| required: false | ||
| default: '5565' | ||
| api-key: | ||
| description: >- | ||
| Optional API key for the engine (passed to the CLI via the | ||
| ROCKETRIDE_APIKEY environment variable, never via argv). | ||
| required: false | ||
| default: '' | ||
| python-version: | ||
| description: 'Python version used to run the rocketride CLI.' | ||
| required: false | ||
| default: '3.12' | ||
| cli-version: | ||
| description: >- | ||
| pip version specifier for the rocketride CLI (e.g. '==1.2.3' or | ||
| '>=1.2.3'). Must resolve to a release that includes the validate | ||
| subcommand; the install step fails fast when it does not. Installs | ||
| the latest release when empty. | ||
| required: false | ||
| default: '' | ||
| fail-on-warnings: | ||
| description: >- | ||
| Fail the job when a pipeline validates with warnings (the CLI itself | ||
| exits 0 on warnings). | ||
| required: false | ||
| default: 'false' | ||
|
|
||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Set up Python | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | ||
| with: | ||
| python-version: ${{ inputs.python-version }} | ||
|
|
||
| - name: Install rocketride CLI | ||
| shell: bash | ||
| env: | ||
| CLI_VERSION: ${{ inputs.cli-version }} | ||
| run: | | ||
| set -euo pipefail | ||
| python -m pip install --quiet --upgrade pip | ||
| python -m pip install --quiet "rocketride${CLI_VERSION}" | ||
| rocketride --help > /dev/null | ||
| # Fail fast when the installed release predates the validate command; | ||
| # otherwise the failure would surface later as a confusing argparse | ||
| # "invalid choice: 'validate'" usage error (exit code 2). | ||
| if ! rocketride validate --help > /dev/null 2>&1; then | ||
| installed="$(python -m pip show rocketride | sed -n 's/^Version: //p')" | ||
| echo "::error::rocketride ${installed:-unknown} does not support the 'validate' subcommand. Set the cli-version input to a release that includes it." | ||
| exit 2 | ||
| fi | ||
|
|
||
| - name: Start RocketRide engine | ||
| id: engine | ||
| shell: bash | ||
| env: | ||
| ENGINE_IMAGE: ${{ inputs.engine-image }} | ||
| ENGINE_PORT: ${{ inputs.engine-port }} | ||
| run: | | ||
| set -euo pipefail | ||
| if ! [[ "${ENGINE_PORT}" =~ ^[0-9]+$ ]]; then | ||
| echo "::error::engine-port must be a number, got '${ENGINE_PORT}'" | ||
| exit 2 | ||
| fi | ||
| # The engine always listens on 5565 inside the container | ||
| # (see docker/docker-compose.yml). | ||
| cid="$(docker run -d -p "${ENGINE_PORT}:5565" "${ENGINE_IMAGE}")" | ||
| echo "cid=${cid}" >> "${GITHUB_OUTPUT}" | ||
| echo "Started engine container ${cid} (${ENGINE_IMAGE}) on host port ${ENGINE_PORT}." | ||
|
|
||
| - name: Wait for engine health | ||
| shell: bash | ||
| env: | ||
| ENGINE_PORT: ${{ inputs.engine-port }} | ||
| ENGINE_CID: ${{ steps.engine.outputs.cid }} | ||
| run: | | ||
| set -euo pipefail | ||
| # /version is the engine's unconditionally public route (server.py | ||
| # registers it outside the standardEndpoints gate), so it works on | ||
| # every published engine image; /ping is gated and 404s on some. | ||
| deadline=$(( SECONDS + 120 )) | ||
| until curl -fsS "http://127.0.0.1:${ENGINE_PORT}/version" > /dev/null 2>&1; do | ||
| if (( SECONDS >= deadline )); then | ||
| echo "::error::RocketRide engine did not become healthy within 120s." | ||
| echo '--- engine logs (last 100 lines) ---' | ||
| docker logs --tail 100 "${ENGINE_CID}" || true | ||
| exit 2 | ||
| fi | ||
| sleep 2 | ||
| done | ||
| echo 'Engine is healthy.' | ||
|
|
||
| - name: Validate pipeline files | ||
| shell: bash | ||
| env: | ||
| FILES_GLOB: ${{ inputs.files }} | ||
| CHANGED_ONLY: ${{ inputs.changed-only }} | ||
| FAIL_ON_WARNINGS: ${{ inputs.fail-on-warnings }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| ROCKETRIDE_URI: http://127.0.0.1:${{ inputs.engine-port }} | ||
| RR_API_KEY_INPUT: ${{ inputs.api-key }} | ||
| run: | | ||
| set -euo pipefail | ||
| shopt -s globstar nullglob | ||
|
|
||
| # Expand the configured glob pattern(s). | ||
| candidates=() | ||
| declare -A seen=() | ||
| read -ra patterns <<< "${FILES_GLOB//$'\n'/ }" | ||
| for pattern in "${patterns[@]}"; do | ||
| # shellcheck disable=SC2086 # intentional glob expansion | ||
| for f in ${pattern}; do | ||
| if [[ -f "${f}" && -z "${seen[${f}]:-}" ]]; then | ||
| seen["${f}"]=1 | ||
| candidates+=("${f}") | ||
| fi | ||
| done | ||
| done | ||
|
|
||
| # On pull requests, optionally narrow to files changed vs the base. | ||
| files=() | ||
| if [[ "${CHANGED_ONLY}" == 'true' && "${EVENT_NAME}" == 'pull_request' && -n "${BASE_SHA}" ]]; then | ||
| if git fetch --quiet --no-tags --depth=1 origin "${BASE_SHA}"; then | ||
| declare -A changed=() | ||
| while IFS= read -r f; do | ||
| if [[ -n "${f}" ]]; then | ||
| changed["${f}"]=1 | ||
| fi | ||
| done < <(git diff --name-only --diff-filter=ACMR "${BASE_SHA}" HEAD) | ||
| for f in ${candidates[@]+"${candidates[@]}"}; do | ||
| if [[ -n "${changed[${f}]:-}" ]]; then | ||
| files+=("${f}") | ||
| fi | ||
| done | ||
| else | ||
| echo "::warning::Could not fetch PR base ${BASE_SHA}; validating all matching files instead." | ||
| files=(${candidates[@]+"${candidates[@]}"}) | ||
| fi | ||
| else | ||
| files=(${candidates[@]+"${candidates[@]}"}) | ||
| fi | ||
|
|
||
| if (( ${#files[@]} == 0 )); then | ||
| echo "::warning::No .pipe files to validate (files='${FILES_GLOB}', changed-only=${CHANGED_ONLY})." | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Pass the API key via the environment only when set, never via argv. | ||
| if [[ -n "${RR_API_KEY_INPUT}" ]]; then | ||
| export ROCKETRIDE_APIKEY="${RR_API_KEY_INPUT}" | ||
| fi | ||
|
|
||
| echo "Validating ${#files[@]} file(s) against ${ROCKETRIDE_URI}:" | ||
| printf ' %s\n' "${files[@]}" | ||
|
|
||
| if [[ "${FAIL_ON_WARNINGS}" == 'true' ]]; then | ||
| report="${RUNNER_TEMP}/rocketride-validate-report.json" | ||
| rc=0 | ||
| rocketride validate "${files[@]}" --json > "${report}" || rc=$? | ||
| cat "${report}" | ||
| if (( rc != 0 )); then | ||
| exit "${rc}" | ||
| fi | ||
| python3 - "${report}" <<'PY' | ||
| import json | ||
| import sys | ||
|
|
||
| with open(sys.argv[1], encoding='utf-8') as fh: | ||
| report = json.load(fh) | ||
|
|
||
| warned = [f['file'] for f in report.get('files', []) if f.get('warnings')] | ||
| for name in warned: | ||
| print(f'::error::fail-on-warnings: {name} validated with warnings') | ||
| sys.exit(1 if warned else 0) | ||
| PY | ||
| else | ||
| rocketride validate "${files[@]}" | ||
| fi | ||
|
|
||
| - name: Stop RocketRide engine | ||
| if: always() | ||
| shell: bash | ||
| env: | ||
| ENGINE_CID: ${{ steps.engine.outputs.cid }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ -n "${ENGINE_CID}" ]]; then | ||
| docker rm -f "${ENGINE_CID}" > /dev/null 2>&1 || true | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Health probe is
/versionrather than/pingdeliberately:server.pyregisters/versionunconditionally public, while/pingsits behind thestandardEndpointsgate — on the published engine image it 404s even with valid auth (verified live). The Helm chart's/pingliveness probes may deserve the same scrutiny; noted in #1572.Also worth a look two steps down:
changed-onlyfetches the PR base SHA explicitly beforegit diff, so the action works withactions/checkout's defaultfetch-depth: 1— no fetch-depth requirement leaks onto consumers.