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:
| Variable | Value |
|---|---|
AUTONOMY_API_URL | Your deployment HTTP base URL, such as https://your-deployment.convex.site, without /api. Do not use .convex.cloud. |
AUTONOMY_TEST_PLAN_ID | The ID copied from Test Plans, containing the cases to run, not an individual test case ID. Use a separate plan per platform. |
PREVIEW_URL | Web only: the final deployed URL, without a login or preview protection page. A deployment step may supply this instead. |
APP_IDENTIFIER | Mobile only: the iOS bundle ID or Android application ID matching the artifact. |
AUTONOMY_WAIT_SECONDS | Optional: 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.
#!/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.
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 webTo 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:
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: falseRun 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 webMobile 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.
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 iosAndroid
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.
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 androidBitrise 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.