Autonomy Docs
Integrations

Jenkins

Trigger Autonomy from Jenkins with a preview URL or mobile artifact and fail the build when the run does not pass.

Add Autonomy to a Jenkins Pipeline after your preview deployment or mobile build. This page includes the API helper and three complete Jenkinsfiles; choose the one for your platform.

Workflow placement

Use a Pipeline job with Pipeline script from SCM, pointing at a trusted branch and the Jenkinsfile in your repository. Commit .ci/autonomy.sh alongside it, create the credentials below, and select Build Now. No webhook or Multibranch Pipeline is required.

In an existing pipeline, put the Autonomy stage after deployment has finished and the preview is reachable. The standalone web example uses an already deployed URL. For a dynamic preview, remove the autonomy-preview-url binding and set env.PREVIEW_URL to your deployment stage's output before this stage.

Install the Pipeline, Pipeline: Declarative, Git, and Credentials Binding plugins and their dependencies. Provision a Linux agent labeled linux with Bash, Git, jq, and curl 7.76 or later. Labels select your agents; Jenkins does not supply hosted Linux or macOS runners.

Required secrets

In Manage Jenkins → Credentials, add Secret text credentials visible to this job. Keep the IDs exactly as shown. Create only the entries needed for your selected platform.

Credential IDValue
autonomy-api-keyAn aut_ organization API key from the deployment you will call.
autonomy-api-urlAPI base URL, such as https://YOUR_DEPLOYMENT.convex.site, without /api.
autonomy-web-plan-idID of your web plan from Test Plans.
autonomy-preview-urlFinal reachable HTTPS preview URL, for the web example.
autonomy-ios-plan-idID of your iOS plan from Test Plans.
autonomy-ios-bundle-idBuilt app's bundle ID, for example com.example.app.
autonomy-android-plan-idID of your Android plan from Test Plans.
autonomy-android-package-nameBuilt APK's application ID, for example com.example.app.
  • Create a test plan containing at least one test case for the selected platform. Use its Test Plans ID, not an individual Test Cases ID.
  • Have a compatible Autonomy runner available. Queued runs do not pass the pipeline until execution finishes.
  • Use cases that do not require runtime values from a saved Environment. These examples send explicit targets and do not select an Environment.
  • Make the preview reachable from the Autonomy runner. A URL that only Jenkins can reach will not work.

withCredentials injects values only around the API call. The single-quoted Groovy shell blocks leave expansion to Bash; keep set +x and never print the key.

Web preview URL

Save this shared helper as .ci/autonomy.sh. All three Jenkinsfiles below use this exact file. It submits the target, prints every queued run ID, and waits for every verdict.

.ci/autonomy.sh
#!/usr/bin/env bash
set -euo pipefail
set +x
: "${AUTONOMY_API_URL:?Set the API base URL, without /api}"
: "${AUTONOMY_API_KEY:?Set an aut_ organization API key}"
: "${AUTONOMY_TEST_PLAN_ID:?Set the Test Plans ID for this platform}"
: "${AUTONOMY_REQUEST_ID:?Set a unique CI job/attempt ID}"
platform="${1:-web}"
api="${AUTONOMY_API_URL%/}"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

post() {
  curl --fail-with-body --silent --show-error \
    --connect-timeout 15 --max-time 90 \
    -X POST "$api$1" \
    -H "Authorization: Bearer $AUTONOMY_API_KEY" \
    -H 'Content-Type: application/json' --data-binary "$2"
}

case "$platform" in
  web)
    : "${PREVIEW_URL:?Set the reachable preview URL after deployment}"
    targets=$(jq -n --arg url "$PREVIEW_URL" '{web:{baseUrl:$url}}')
    ;;
  ios|android)
    : "${ARTIFACT_PATH:?Set the path to the app archive or APK}"
    : "${APP_IDENTIFIER:?Set the bundle ID or Android package name}"
    test -s "$ARTIFACT_PATH"
    file=$(basename "$ARTIFACT_PATH")
    bytes=$(wc -c < "$ARTIFACT_PATH" | tr -d '[:space:]')
    content_type=application/zip
    if [ "$platform" = android ]; then
      content_type=application/vnd.android.package-archive
    fi
    upload=$(post /api/artifacts/upload-url "$(jq -n \
      --arg platform "$platform" --arg fileName "$file" \
      --arg contentType "$content_type" --argjson declaredBytes "$bytes" \
      '{platform:$platform,fileName:$fileName,contentType:$contentType,declaredBytes:$declaredBytes}')")
    url=$(jq -er '.uploadUrl | strings | select(length > 0)' <<< "$upload")
    storage=$(jq -er '.storageId | strings | select(length > 0)' <<< "$upload")
    intent=$(jq -er '.intentId | strings | select(length > 0)' <<< "$upload")
    jq -e '.method == "PUT"' <<< "$upload" > /dev/null
    curl --fail-with-body --silent --show-error \
      --connect-timeout 15 --max-time 600 -X PUT "$url" \
      -H "Content-Type: $content_type" --data-binary "@$ARTIFACT_PATH"
    scan=$(post /api/artifacts/scan "$(jq -n \
      --arg storageId "$storage" --arg intentId "$intent" \
      '{storageId:$storageId,intentId:$intentId}')")
    if ! jq -e '.status == "clean"' <<< "$scan" > /dev/null; then
      echo 'Artifact scan did not pass; refusing to trigger.' >&2
      exit 1
    fi
    targets=$(jq -n --arg platform "$platform" --arg storageId "$storage" \
      --arg fileName "$file" --arg identifier "$APP_IDENTIFIER" \
      '{($platform):({storageId:$storageId,sourceMode:"upload",fileName:$fileName} +
        (if $platform == "ios" then {bundleId:$identifier} else {packageName:$identifier} end))}')
    ;;
  *) echo 'Usage: bash .ci/autonomy.sh web|ios|android' >&2; exit 2 ;;
esac

body=$(jq -n --arg testPlanId "$AUTONOMY_TEST_PLAN_ID" \
  --arg platform "$platform" --argjson targets "$targets" \
  --arg branch "${AUTONOMY_BRANCH:-manual}" \
  --arg commitSha "${AUTONOMY_COMMIT_SHA:-}" \
  --arg key "$AUTONOMY_REQUEST_ID:$platform" \
  '{testPlanId:$testPlanId,platforms:[$platform],targets:$targets,
    branch:$branch,deployment:{branch:$branch,commitSha:$commitSha},idempotencyKey:$key}')
post /api/v1/run.trigger "$body" > "$work/trigger.json"
jq -e '.runIds | type == "array" and length > 0' "$work/trigger.json" > /dev/null
jq -er '.runIds[] | strings | select(length > 0)' "$work/trigger.json" > "$work/run-ids"
printf 'Queued Autonomy runs:\n'
cat "$work/run-ids"

deadline=$((SECONDS + ${AUTONOMY_WAIT_SECONDS:-1200}))
while IFS= read -r run_id; do
  while :; do
    if (( SECONDS >= deadline )); then
      echo "Timed out waiting for $run_id; failing the CI job." >&2
      exit 1
    fi
    result=$(post /api/v1/run.get "$(jq -n --arg runId "$run_id" '{runId:$runId}')")
    status=$(jq -er '.status' <<< "$result")
    case "$status" in
      queued|running|retrying) sleep 10 ;;
      passing|failed|unverified|invalid|canceled)
        printf '%s: %s\n' "$run_id" "$status"
        if ! jq -e '.verdict == "passed" and .validity == "valid"' <<< "$result" > /dev/null; then
          echo "Autonomy gate failed for $run_id." >&2
          exit 1
        fi
        break ;;
      *) echo "Unexpected run status: $status" >&2; exit 1 ;;
    esac
  done
done < "$work/run-ids"
echo 'Every Autonomy run passed.'

Save this as Jenkinsfile for a web job. The four web credentials above make the first manual build runnable without job parameters.

Jenkinsfile
pipeline {
  agent { label 'linux' }
  options {
    skipDefaultCheckout(true)
    timeout(time: 30, unit: 'MINUTES')
  }
  stages {
    stage('Checkout') {
      steps { checkout scm }
    }
    stage('Autonomy web') {
      steps {
        withCredentials([
          string(credentialsId: 'autonomy-api-key', variable: 'AUTONOMY_API_KEY'),
          string(credentialsId: 'autonomy-api-url', variable: 'AUTONOMY_API_URL'),
          string(credentialsId: 'autonomy-web-plan-id', variable: 'AUTONOMY_TEST_PLAN_ID'),
          string(credentialsId: 'autonomy-preview-url', variable: 'PREVIEW_URL')
        ]) {
          sh '''#!/usr/bin/env bash
set -euo pipefail
set +x
export AUTONOMY_REQUEST_ID="jenkins:${JOB_NAME}:${BUILD_NUMBER}"
export AUTONOMY_BRANCH="${BRANCH_NAME:-${GIT_BRANCH:-manual}}"
export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
bash .ci/autonomy.sh web
'''
        }
      }
    }
  }
}

Mobile artifact handoff

Use a separate test plan for each platform. Keep .ci/autonomy.sh from the web section and replace Jenkinsfile with the relevant example. The helper requests an upload URL, uploads the archive, requires a clean artifact scan, and then triggers the plan.

iOS

Provision a real macOS agent labeled macos with Xcode, its Simulator SDK, Git, and zip. Jenkins has no free hosted macOS tier. Use your existing Mac; provisioning paid capacity is a separate decision. Resolve your project's dependencies before xcodebuild, and replace MyApp, its workspace path, and output name with your project's values.

Build a Simulator .app, not a device .ipa. zip -y keeps symlinks, and the archive retains executable permissions. stash passes that archive to the Linux stage; Linux uploads it without unpacking it. For large apps, configure a remote Jenkins artifact manager so stashing does not burden the controller.

Jenkinsfile
pipeline {
  agent none
  options { skipDefaultCheckout(true) }
  stages {
    stage('Build iOS Simulator app') {
      agent { label 'macos' }
      options { timeout(time: 60, unit: 'MINUTES') }
      steps {
        checkout scm
        sh '''#!/usr/bin/env bash
set -euo pipefail
xcodebuild \
  -workspace ios/MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Release \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath ios/build \
  CODE_SIGNING_ALLOWED=NO build
mkdir -p .build
rm -f .build/MyApp.app.zip
(cd ios/build/Build/Products/Release-iphonesimulator && \
  zip -qry "$WORKSPACE/.build/MyApp.app.zip" MyApp.app)
'''
        stash name: 'ios-simulator', includes: '.build/MyApp.app.zip'
      }
    }
    stage('Autonomy iOS') {
      agent { label 'linux' }
      options { timeout(time: 35, unit: 'MINUTES') }
      steps {
        checkout scm
        unstash 'ios-simulator'
        withCredentials([
          string(credentialsId: 'autonomy-api-key', variable: 'AUTONOMY_API_KEY'),
          string(credentialsId: 'autonomy-api-url', variable: 'AUTONOMY_API_URL'),
          string(credentialsId: 'autonomy-ios-plan-id', variable: 'AUTONOMY_TEST_PLAN_ID'),
          string(credentialsId: 'autonomy-ios-bundle-id', variable: 'APP_IDENTIFIER')
        ]) {
          sh '''#!/usr/bin/env bash
set -euo pipefail
set +x
export ARTIFACT_PATH='.build/MyApp.app.zip'
export AUTONOMY_REQUEST_ID="jenkins:${JOB_NAME}:${BUILD_NUMBER}"
export AUTONOMY_BRANCH="${BRANCH_NAME:-${GIT_BRANCH:-manual}}"
export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
bash .ci/autonomy.sh ios
'''
        }
      }
    }
  }
}

Android

Provision an agent labeled linux-android with Bash, Git, jq, curl 7.76 or later, a JDK compatible with your Gradle version, and the Android SDK packages your project needs. Set JAVA_HOME and ANDROID_HOME on the agent and accept the SDK licenses. The example assumes an executable Gradle wrapper at android/gradlew; adjust the directory and APK path for your project. Build and upload use the same workspace.

Jenkinsfile
pipeline {
  agent { label 'linux-android' }
  options {
    skipDefaultCheckout(true)
    timeout(time: 60, unit: 'MINUTES')
  }
  stages {
    stage('Build Android APK') {
      steps {
        checkout scm
        sh '''#!/usr/bin/env bash
set -euo pipefail
cd android
./gradlew --no-daemon assembleDebug
'''
      }
    }
    stage('Autonomy Android') {
      steps {
        withCredentials([
          string(credentialsId: 'autonomy-api-key', variable: 'AUTONOMY_API_KEY'),
          string(credentialsId: 'autonomy-api-url', variable: 'AUTONOMY_API_URL'),
          string(credentialsId: 'autonomy-android-plan-id', variable: 'AUTONOMY_TEST_PLAN_ID'),
          string(credentialsId: 'autonomy-android-package-name', variable: 'APP_IDENTIFIER')
        ]) {
          sh '''#!/usr/bin/env bash
set -euo pipefail
set +x
export ARTIFACT_PATH='android/app/build/outputs/apk/debug/app-debug.apk'
export AUTONOMY_REQUEST_ID="jenkins:${JOB_NAME}:${BUILD_NUMBER}"
export AUTONOMY_BRANCH="${BRANCH_NAME:-${GIT_BRANCH:-manual}}"
export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
bash .ci/autonomy.sh android
'''
        }
      }
    }
  }
}

Gate the pipeline on the verdict

The helper calls POST /api/v1/run.trigger, then polls POST /api/v1/run.get for every returned runIds entry. A queued response only means the request was accepted. Success requires every terminal result to contain verdict: "passed" and validity: "valid".

An API error, rejected scan, unexpected status, failed or unverified result, cancellation, or timeout produces a nonzero exit, which fails the Jenkins stage. The default polling budget is 1,200 seconds across all returned runs; set AUTONOMY_WAIT_SECONDS higher for longer plans and increase the Jenkins timeout accordingly. A pipeline timeout does not cancel an already queued Autonomy run.

AUTONOMY_REQUEST_ID combines the Jenkins job and build number, with the platform added by the helper. Retrying the trigger in the same build reuses its idempotency key; a new Jenkins build gets a new key.

Branch and event strategy

Start with manual builds from a trusted default branch. Once the deployment handoff works, add your existing SCM trigger or schedule to the job. Use short smoke plans for trusted change builds and larger plans for release or scheduled builds.

Credentials are available to the checked-out scripts. Do not run an untrusted pull request's Jenkinsfile or helper with these credentials, even if its branch name matches a filter. Use a trusted job to test an approved preview URL. Keep build agents that hold these credentials separate from agents running untrusted jobs.

Vendor references

The examples use the documented Declarative Pipeline syntax, Secret text bindings, stash and unstash steps, and Jenkins agents. See Using a Jenkinsfile for Pipeline from SCM setup.

On this page