For the complete documentation index, see llms.txt.
Skip to main content

Automate reShapr with the CLI in CI/CD

Use this guide to import or update an API definition from a CI job without parsing human-readable output. The workflow authenticates through standard input, records existing state, runs the idempotent import path, and fails unless the resulting Service, Configuration Plan, and Exposition satisfy explicit postconditions.

reShapr provides scriptable CLI and API operations. It does not provide a native CI/CD pipeline, automatic drift detection, deployment approval, or rollback orchestration.

Prerequisites​

You need:

  • Node.js 20 or later and reShapr CLI 0.2.3 installed in the job image;
  • a reShapr 0.2.3 control plane and a running Gateway in the default Gateway Group;
  • a dedicated automation identity restricted to the target organization;
  • the identity password stored as a masked CI secret;
  • an OpenAPI 3, GraphQL, or Protobuf source tracked by the build;
  • a backend endpoint reachable from the selected Gateway;
  • Bash and jq.

This example uses password authentication for an on-premises control plane. Adapt the authentication stage when your deployment uses another supported identity flow.

Set non-sensitive job inputs as CI variables:

export RESHAPR_SERVER='https://app.reshapr.example.com'
export RESHAPR_USERNAME='ci-release'
export RESHAPR_ORGANIZATION='production'
export SERVICE_NAME='Weather API'
export SERVICE_VERSION='1.0.0'
export API_DEFINITION='openapi/weather.yaml'
export BACKEND_ENDPOINT='https://weather-api.production.example.com'

Expose the password to the job as RESHAPR_PASSWORD through the CI platform's secret mechanism. Do not commit it or include it in a command argument.

Prepare an isolated CLI home​

The CLI stores its session under $HOME/.reshapr/config. Give each job an isolated home directory and restrict its permissions:

export HOME="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/reshapr-ci-${CI_JOB_ID:-$$}"
mkdir -p "${HOME}"
chmod 700 "${HOME}"

Do not cache or publish this directory as a job artifact. Remove it in the CI platform's unconditional cleanup stage.

Authenticate without a password argument​

Disable shell tracing before reading or forwarding a secret. Pipe the password to --password-stdin:

set +x
printf '%s\n' "${RESHAPR_PASSWORD}" \
| reshapr login \
--username "${RESHAPR_USERNAME}" \
--password-stdin \
--org "${RESHAPR_ORGANIZATION}" \
--server "${RESHAPR_SERVER}"
unset RESHAPR_PASSWORD
reshapr switch-org "${RESHAPR_ORGANIZATION}"

The CLI writes its bearer token to a mode-0600 configuration file. switch-org replaces it with a token for the target organization and fails if the user is not a member. Confirm that subsequent authenticated calls work:

reshapr info

An invalid credential, unreachable server, expired token, or malformed response causes the CLI to exit non-zero. Stop the job rather than treating those failures as an absent resource.

Record existing state​

Start the deployment stage in strict mode:

set -euo pipefail

Request structured output and normalize the empty-account case because service list --output json writes no document when the organization has no Services:

SERVICE_LIST="$(reshapr service list --output json)"
if [[ -z "${SERVICE_LIST}" ]]; then
SERVICE_LIST='[]'
fi

EXISTING_SERVICE_ID="$(
jq -er \
--arg name "${SERVICE_NAME}" \
--arg version "${SERVICE_VERSION}" \
'first(.[] | select(.name == $name and .version == $version) | .id) // ""' \
<<<"${SERVICE_LIST}"
)"

if [[ -n "${EXISTING_SERVICE_ID}" ]]; then
printf 'Updating Service %s\n' "${EXISTING_SERVICE_ID}"
else
printf 'Creating Service %s:%s\n' "${SERVICE_NAME}" "${SERVICE_VERSION}"
fi

An empty lookup is expected state. A failed reshapr service list is not: strict mode stops the job before any deployment command runs.

Import or update the Service​

Run the same command for creation and update:

IMPORT_RESULT="$(
reshapr import \
--file "${API_DEFINITION}" \
--serviceName "${SERVICE_NAME}" \
--serviceVersion "${SERVICE_VERSION}" \
--backendEndpoint "${BACKEND_ENDPOINT}" \
--output json
)"

For the same Service name and version, release 0.2.3 updates the imported Service instead of creating a duplicate. With --backendEndpoint, it creates the default Configuration Plan and Exposition when absent, then reuses them on later imports.

Validate the complete structured result before using any identifier:

jq -e \
--arg name "${SERVICE_NAME}" \
--arg version "${SERVICE_VERSION}" \
--arg backend "${BACKEND_ENDPOINT}" \
'.service.id != null
and .service.name == $name
and .service.version == $version
and .configurationPlan.id != null
and .configurationPlan.name == "default"
and .configurationPlan.backendEndpoint == $backend
and .exposition.id != null
and (.endpoints | length > 0)' \
<<<"${IMPORT_RESULT}" >/dev/null

export SERVICE_ID="$(jq -er '.service.id' <<<"${IMPORT_RESULT}")"
export CONFIGURATION_PLAN_ID="$(jq -er '.configurationPlan.id' <<<"${IMPORT_RESULT}")"
export EXPOSITION_ID="$(jq -er '.exposition.id' <<<"${IMPORT_RESULT}")"
unset IMPORT_RESULT SERVICE_LIST

If the existing default Plan targets another backend, import --backendEndpoint deliberately retains that value. The assertion then fails. Changing an existing Plan through reshapr config update opens an interactive editor in 0.2.3; use a separately reviewed API operation or a controlled replacement workflow instead of automating the editor.

Verify the postconditions​

Confirm that a previous lookup, when present, resolved to the same Service:

if [[ -n "${EXISTING_SERVICE_ID}" && "${EXISTING_SERVICE_ID}" != "${SERVICE_ID}" ]]; then
printf 'Service identity changed unexpectedly\n' >&2
exit 1
fi

Fetch the Service and Exposition independently. service get emits the Service directly. expo get combines the Exposition with its active Gateways and computed endpoints:

reshapr service get "${SERVICE_ID}" --output json \
| jq -e \
--arg id "${SERVICE_ID}" \
--arg name "${SERVICE_NAME}" \
--arg version "${SERVICE_VERSION}" \
'.id == $id and .name == $name and .version == $version' \
>/dev/null

EXPOSITION="$(reshapr expo get "${EXPOSITION_ID}" --output json)"
jq -e \
--arg id "${EXPOSITION_ID}" \
--arg serviceId "${SERVICE_ID}" \
--arg planId "${CONFIGURATION_PLAN_ID}" \
'.exposition.id == $id
and .exposition.service.id == $serviceId
and .exposition.configurationPlan.id == $planId
and (.gateways | length > 0)' \
<<<"${EXPOSITION}" >/dev/null

export MCP_URL="$(jq -er '.endpoints[0]' <<<"${EXPOSITION}")"
unset EXPOSITION EXISTING_SERVICE_ID
printf 'Verified active MCP endpoint: %s\n' "${MCP_URL}"

Finish with Test an MCP Endpoint. A real read-only tools/call validates the Gateway route and backend behavior that control-plane resource checks cannot prove.

Control destructive changes​

Do not use --force as the impact-assessment mechanism. For an Artifact removal, query the public deletion-impact endpoint first and evaluate its structured response against your deployment policy.

The following helper sends the saved bearer token through curl configuration on standard input, keeping it out of the curl argument list:

export ARTIFACT_ID='<artifact-id-approved-for-removal>'
RESHAPR_TOKEN="$(jq -er '.token' "${HOME}/.reshapr/config")"

set +x
DELETION_IMPACT="$(
{
printf 'header = "Authorization: Bearer %s"\n' "${RESHAPR_TOKEN}"
printf 'url = "%s/api/v1/artifacts/%s/deletion-impact"\n' \
"${RESHAPR_SERVER}" "${ARTIFACT_ID}"
} | curl --silent --show-error --fail-with-body --config -
)"
unset RESHAPR_TOKEN

This example permits deletion only when no Configuration Plan references the Artifact:

jq -e '(.impactedPlans // []) | length == 0' \
<<<"${DELETION_IMPACT}" >/dev/null

reshapr artifact delete "${ARTIFACT_ID}" --force
unset DELETION_IMPACT

Replace the jq expression with a reviewed policy when selected Plans may be changed. Deleting the last selected Artifact from a Plan can make that Plan fall back to all attached Artifacts, widening its MCP surface. Verify affected Plans and tools/list after any approved deletion.

The CLI reports expected absence and other HTTP failures with the same non-zero exit status for many commands. When a pipeline must distinguish HTTP 404 from authentication, authorization, validation, quota, or server failures, call the release-tagged public API contract directly and branch on its status code. Do not infer absence from an arbitrary CLI failure message.

Clean up job credentials​

Remove the isolated CLI state from an unconditional cleanup stage, including after a failed deployment:

rm -rf "${HOME}/.reshapr"

Use the CI platform's protected workspace cleanup rather than relying only on this final command.

Result​

The job authenticates without a password argument, observes existing state, imports or updates one Service through structured CLI output, preserves the default Plan and Exposition identities, and fails when its explicit postconditions are not met.

Limits​

  • This guide supplies a portable Bash workflow, not a ready-made pipeline for a specific CI product.
  • import --backendEndpoint reuses an existing default Plan and does not update its backend endpoint.
  • reshapr config update is interactive in 0.2.3 and is unsuitable for an unattended job.
  • The default import-and-expose path targets the built-in default Gateway Group. Use explicit Plan and Exposition API operations when another group is required.
  • The CLI does not provide a universal create-or-update command for every resource type.
  • Structured output does not provide transactionality, locking, drift detection, approval, retry, or rollback semantics.
  • Concurrent jobs targeting the same resource require serialization or conflict handling owned by the pipeline.

Next step​

Use Manage reShapr Resources with GitOps when Kubernetes controllers should own desired state. Review Product Interfaces to choose between the CLI, public APIs, Web UI, and Kubernetes controllers.

The release-tagged CLI implementation and public API contract remain the canonical interface references.

Agent View