Autonomy Docs
Integrations

Bitrise

Trigger and gate Bitrise workflows on Autonomy runs for a web preview, iOS Simulator app, or Android APK.

Use a Bitrise Script Step to upload a mobile build or test a deployed web preview, then keep the workflow open until Autonomy returns a verdict.

Workflow placement

Add the Autonomy step after your existing build and deployment steps. For web, export the final public preview URL as PREVIEW_URL from the deployment step with envman add --key PREVIEW_URL --value "$PREVIEW_URL"; subsequent steps can read it. For mobile, keep the build and upload in the same workflow so the artifact stays in the workspace.

The complete configurations below are alternatives for bitrise.yml. Copy the one for your platform, or merge its steps into your existing workflow. Configure Bitrise to read bitrise.yml from the repository and commit .ci/autonomy.sh alongside it. The web workflow can also run locally in a checked-out repository.

Required secrets

Before the first run

  • Create an Autonomy organization API key beginning with aut_ in Settings → API Keys, in the organization containing your plan.
  • Create a Test Plan in Test Plans with at least one runnable test case for the selected platform, and copy its ID.
  • Use test cases that do not reference environment runtime variables for this explicit-target recipe.
  • Have an available Autonomy runner for the platform and a preview URL reachable from that runner.

In Workflows → Secrets, add AUTONOMY_API_KEY. Leave Expose for pull requests off and Replace variables in inputs unchecked. Secrets become shell environment variables automatically; do not put the key in bitrise.yml.

In Workflows → Env Vars, set these values for the selected workflow:

VariableValue
AUTONOMY_API_URLYour deployment HTTP base URL, such as https://your-deployment.convex.site, without /api. Do not use .convex.cloud.
AUTONOMY_TEST_PLAN_IDThe ID copied from Test Plans, containing the cases to run, not an individual test case ID. Use a separate plan per platform.
PREVIEW_URLWeb only: the final deployed URL, without a login or preview protection page. A deployment step may supply this instead.
APP_IDENTIFIERMobile only: the iOS bundle ID or Android application ID matching the artifact.
AUTONOMY_WAIT_SECONDSOptional: gate timeout in seconds, default 1200.

Web preview URL

Create a .ci directory in the repository and save this entire script as .ci/autonomy.sh. It needs Bash, curl 7.76 or newer, and jq; the selected hosted stacks provide them. Install these tools yourself for a local CLI run. Keep this file identical for every platform.

.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 configuration as bitrise.yml. Set the workflow variables above, then choose Start build → web after deploying the preview. The checkout step runs on Bitrise; the local CLI uses the repository you already checked out.

bitrise.yml
format_version: '13'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
workflows:
  web:
    meta:
      bitrise.io:
        stack: linux-docker-android-22.04
        machine_type_id: standard
    steps:
      - activate-ssh-key@4:
          run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
      - git-clone@8:
          run_if: '{{getenv "BITRISE_IO" | eq "true"}}'
      - script@1:
          title: Autonomy
          inputs:
          - content: |-
              #!/usr/bin/env bash
              set -euo pipefail
              set +x
              : "${PREVIEW_URL:?Set the deployed preview URL}"
              curl --fail --silent --show-error --retry 12 \
                --retry-delay 5 --retry-connrefused --max-time 20 \
                "$PREVIEW_URL" > /dev/null
              attempt="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)"
              export AUTONOMY_REQUEST_ID="bitrise:${BITRISE_BUILD_SLUG:-local}:${BITRISE_TRIGGERED_WORKFLOW_ID}:$attempt"
              export AUTONOMY_BRANCH="${BITRISE_GIT_BRANCH:-$(git branch --show-current)}"
              export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
              bash .ci/autonomy.sh web

To use the local runner, install the Bitrise CLI, then save this as .bitrise.secrets.yml beside bitrise.yml. Replace all four example values with your API key, deployment URL, plan ID, and reachable preview URL:

.bitrise.secrets.yml
envs:
- AUTONOMY_API_KEY: 'aut_REPLACE_WITH_ORG_KEY'
  opts:
    is_expand: false
- AUTONOMY_API_URL: 'https://your-deployment.convex.site'
  opts:
    is_expand: false
- AUTONOMY_TEST_PLAN_ID: 'REPLACE_WITH_TEST_PLANS_ID'
  opts:
    is_expand: false
- PREVIEW_URL: 'https://your-preview.example.com'
  opts:
    is_expand: false

Run these commands from the repository root. Keep the secrets file out of version control. The local CLI runs on your host; the meta stack selection applies only to hosted builds.

printf '\n.bitrise*\n' >> .gitignore
chmod 600 .bitrise.secrets.yml
bitrise setup
bitrise run web

Mobile artifact handoff

Use the same .ci/autonomy.sh above. It requests an upload URL, sends the binary with PUT, requires a clean scan, and triggers the run with the returned storage ID. The build and upload share the same workspace; there is no cross-workflow artifact transfer to configure.

iOS

This complete alternative bitrise.yml uses a macOS Xcode stack. Set AUTONOMY_TEST_PLAN_ID to your iOS plan and APP_IDENTIFIER to its bundle ID. Replace ios/MyApp.xcodeproj, MyApp, and the resulting .app name with your project, shared scheme, and product name. If your app uses a workspace, replace -project ios/MyApp.xcodeproj with -workspace ios/MyApp.xcworkspace. Keep any existing dependency installation steps before xcodebuild.

Build a Simulator .app, then zip it; a device .ipa cannot be used here. The upload and verdict wait also run on macOS and consume its build time.

bitrise.yml
format_version: '13'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
workflows:
  ios:
    meta:
      bitrise.io:
        stack: osx-xcode-26.4.x
        machine_type_id: g2.mac.medium
    steps:
      - activate-ssh-key@4:
          run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
      - git-clone@8:
          run_if: '{{getenv "BITRISE_IO" | eq "true"}}'
      - script@1:
          title: Autonomy
          inputs:
          - content: |-
              #!/usr/bin/env bash
              set -euo pipefail
              set +x
              xcodebuild -project ios/MyApp.xcodeproj -scheme MyApp \
                -configuration Release -sdk iphonesimulator \
                -destination 'generic/platform=iOS Simulator' \
                -derivedDataPath .build/ios CODE_SIGNING_ALLOWED=NO build
              mkdir -p .build/artifacts
              ditto -c -k --sequesterRsrc --keepParent \
                .build/ios/Build/Products/Release-iphonesimulator/MyApp.app \
                .build/artifacts/MyApp.app.zip
              export ARTIFACT_PATH="$PWD/.build/artifacts/MyApp.app.zip"
              attempt="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)"
              export AUTONOMY_REQUEST_ID="bitrise:${BITRISE_BUILD_SLUG:-local}:${BITRISE_TRIGGERED_WORKFLOW_ID}:$attempt"
              export AUTONOMY_BRANCH="${BITRISE_GIT_BRANCH:-$(git branch --show-current)}"
              export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
              bash .ci/autonomy.sh ios

Android

This complete alternative bitrise.yml uses the Linux Android stack. Set AUTONOMY_TEST_PLAN_ID to your Android plan and APP_IDENTIFIER to its application ID. It assumes an Android project in android/, a checked-in Gradle wrapper, and the app module. Adjust the directory, task, and APK path together if your project uses a different module or build variant. Keep the JDK and Android SDK versions required by your existing build.

bitrise.yml
format_version: '13'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
workflows:
  android:
    meta:
      bitrise.io:
        stack: linux-docker-android-22.04
        machine_type_id: standard
    steps:
      - activate-ssh-key@4:
          run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
      - git-clone@8:
          run_if: '{{getenv "BITRISE_IO" | eq "true"}}'
      - script@1:
          title: Autonomy
          inputs:
          - content: |-
              #!/usr/bin/env bash
              set -euo pipefail
              set +x
              (cd android && bash ./gradlew --no-daemon assembleDebug)
              export ARTIFACT_PATH="$PWD/android/app/build/outputs/apk/debug/app-debug.apk"
              attempt="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)"
              export AUTONOMY_REQUEST_ID="bitrise:${BITRISE_BUILD_SLUG:-local}:${BITRISE_TRIGGERED_WORKFLOW_ID}:$attempt"
              export AUTONOMY_BRANCH="${BITRISE_GIT_BRANCH:-$(git branch --show-current)}"
              export AUTONOMY_COMMIT_SHA="$(git rev-parse HEAD)"
              bash .ci/autonomy.sh android

Bitrise currently lists macOS Medium and Linux Medium for its free Hobby plan, with 300 credits per month and a 90-minute build timeout. The examples select g2.mac.medium and standard, respectively. A local bitrise run ios still requires your own Mac with Xcode; selecting a macOS stack in YAML does not create a remote Mac. Check your remaining credits before hosted mobile builds. See Bitrise pricing.

Gate the pipeline on the verdict

The helper prints each queued run ID and polls POST /api/v1/run.get. A queued response only confirms dispatch. The workflow succeeds only when every run has verdict: "passed" and validity: "valid". Failed, unverified, invalid, canceled, unknown, timed-out, or HTTP-error results fail the Script Step and stop subsequent normal steps. Do not add is_skippable: true to this step.

The default wait is 20 minutes; increase AUTONOMY_WAIT_SECONDS within your Bitrise build timeout if your plan needs longer. A gate timeout does not cancel the Autonomy run. Open the run in Runs using the printed ID to inspect its evidence. Each workflow invocation creates a fresh UUID alongside the build slug and workflow ID, so a retry or local invocation queues a new run.

Branch and event strategy

The examples are manual workflows with no automatic trigger, so setup does not expose an API key to untrusted code. Start with a small smoke plan, then configure a push trigger for a trusted staging or release branch in Bitrise. Add the Autonomy step after deployment in that workflow, preserving its branch filters. Reserve larger plans for scheduled or manual runs.

Keep Secrets unavailable to pull request builds, including forked pull requests. Run QA on reviewed code after it reaches a trusted branch, or manually start a build of a reviewed commit. Do not enable pull request secret exposure to make a failing recipe pass. Branch and commit metadata are sent to Autonomy; this recipe does not create a pull request comment.

Vendor references

On this page