| title | Upload builds |
|---|---|
| description | Every way to upload an Android or iOS build to QA Wolf, how a run picks the build it installs, and how to move from legacy uploads. |
| sidebarTitle | Upload builds |
| keywords | build, app build, apk, aab, ipa, upload a build, upload-build-action, uploadBuild, qawolf build upload, upload_build_to_qawolf, build.requestUpload, build.create, RUN_INPUT_PATH, app version |
A build is an Android .apk or .aab, or an iOS .ipa, that you upload to QA Wolf. QA Wolf reads the app id, version and build number from the file. Every run records which build it installed, so you know exactly what each run tested.
- File types —
.apkand.aabfor Android,.ipafor iOS. Any other file is refused, including a.zip. - Size — at most 1 GB.
- File name — letters, digits,
.,_and-, at most 255 characters. The Builds page and the CLI replace other characters with-, and the Fastlane plugin replaces spaces with-. Every other way refuses the file. - Zipped apps — not accepted. Upload the
.apk,.aabor.ipainside the zip. A zip that holdsPayload/<App>.app/is already an.ipa, so rename it to.ipa. Any other zipped app stays on legacy build uploads for now. - Retention — 180 days after upload, except the latest build of each app and platform in a static environment, which stays until a later upload replaces it.
A build reaches runs in three ways:
- Through its environment — upload the build with an environment, and it becomes that environment's latest build of its app and platform. Every later run in the environment installs it, until a later upload of the same app and platform replaces it. This covers scheduled runs, runs started by hand, and runs a deployment starts.
- Through a run or a deployment — name the build when you create a run or report a deployment. Only those runs install it. A build uploaded without an environment reaches runs only this way.
- Through Tester chat — a build sent in chat reaches the runs in that chat session.
"Latest" means uploaded last, not the highest version. Uploading an older version again makes it the latest.
For each app and platform, a run installs the first build it finds in this order:
- The builds named when the run was created. See Test a build in one run.
- The builds the deployment that started the run reported.
- The environment's latest build.
- The base environment's latest build, for an environment that has one.
A build sent in Tester chat comes before all of these, for the runs in that chat session. A build whose file has expired counts as absent.
Each way below uploads the file and creates the build in one step.
The Builds page, Tester chat and the [QA Wolf MCP](/qawolf-mcp) use your sign-in, and the CLI accepts `qawolf auth login`. Every other way needs an API key. Create one under **Workspace Settings → API Keys**. An organization or user key also needs the workspace id, shown on the same page. Open **Settings → Builds** and click **Upload build**, or drop the file on the page. Choose the static environment whose runs install the build. It defaults to your default environment. Click **Add build** once the file finishes uploading.Use qawolf/upload-build-action@v1. Pass its build-id output to qawolf/notify-qawolf-on-deploy-action@v3, version 3.2.0 or later. The runs the deployment starts then install the build.
Example:
- name: Upload the build to QA Wolf
id: upload-build
uses: qawolf/upload-build-action@v1
with:
qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
workspace-id: ${{ vars.QAWOLF_WORKSPACE_ID }}
input-file-path: app/build/outputs/apk/release/app-release.apk
environment: Staging
- name: Report the deployment to QA Wolf
uses: qawolf/notify-qawolf-on-deploy-action@v3
with:
qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
workspace-id: ${{ vars.QAWOLF_WORKSPACE_ID }}
environment: Staging
service: android
builds: ${{ steps.upload-build.outputs.build-id }}The upload action also outputs app-id, platform, version and build-number.
To test a pull request in its own ephemeral environment, set environment to a name for the pull request, such as pr-${{ github.event.pull_request.number }}, in both steps. On the upload step, also set ephemeral-environment: "true" and base-environment. See the action's README for every input.
Use the QA Wolf Fastlane plugin 1.2.0 or later. The plugin is not published to RubyGems. Point the Gemfile at the repository and pin a tag:
gem "fastlane-plugin-qawolf", git: "https://github.com/qawolf/fastlane-plugin-qawolf", tag: "1.2.0"upload_build_to_qawolf uploads the file that gradle or build_app built earlier in the lane. notify_deploy_qawolf reports every build the lane uploaded, so you do not pass the build id:
lane :qawolf do
# Your existing build step, gradle for Android or build_app for iOS
upload_build_to_qawolf(
# Also read from QAWOLF_API_KEY and QAWOLF_WORKSPACE_ID
qawolf_api_key: ENV.fetch("QAWOLF_API_KEY", nil),
workspace_id: ENV.fetch("QAWOLF_WORKSPACE_ID", nil),
environment: "Staging"
)
notify_deploy_qawolf(
qawolf_api_key: ENV.fetch("QAWOLF_API_KEY", nil),
workspace_id: ENV.fetch("QAWOLF_WORKSPACE_ID", nil),
environment: "Staging",
service: "android"
)
endFor a pull request, pass ephemeral_environment: true and base_environment to upload_build_to_qawolf. The plugin's README is the reference for every option.
Use uploadBuild from @qawolf/ci-sdk 3.5.0 or later. Pass the build id to deployment.reportStatus as builds:
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
const { deployment, uploadBuild } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
const uploaded = await uploadBuild({
filePath: "app/build/outputs/apk/release/app-release.apk",
environment: { name: "Staging" },
workspaceId: process.env.QAWOLF_WORKSPACE_ID,
});
if (uploaded.outcome !== "uploaded") {
throw new Error(`Upload failed: ${JSON.stringify(uploaded)}`);
}
const report = await deployment.reportStatus({
workspaceId: process.env.QAWOLF_WORKSPACE_ID,
providerDeploymentId: `${process.env.CI_BUILD_ID}-android`,
status: "success",
environment: { name: "Staging" },
service: "android",
builds: [uploaded.build.id],
});
if (report.outcome !== "reported") {
throw new Error(`Report failed: ${JSON.stringify(report)}`);
}uploadBuild returns an outcome rather than throwing. Each call creates a new build, so a retry is another call.
Use qawolf build upload from @qawolf/cli 2.8.0 or later:
npx @qawolf/cli build upload app-release.apk \
--environment-name Staging \
--branch main \
--commit-sha "$GIT_COMMIT"Set QAWOLF_API_KEY, or sign in with qawolf auth login. An organization or user key also needs --workspace-id. Use --environment-id in place of --environment-name to name an environment by id. The command prints the build id and the build's URL.
For a pipeline that cannot run Node.js. Each call is a POST to https://app.qawolf.com/api/trpc/public.<name> with the input under json.
The response holds `buildId`, `uploadUrl` and `uploadHeaders` under `result.data.json`.
```bash
HEADERS=()
while IFS= read -r header; do HEADERS+=(-H "$header"); done < <(
echo "$UPLOAD" | jq -r '.result.data.json.uploadHeaders | to_entries[] | "\(.key): \(.value)"'
)
curl -sf -X PUT --upload-file app-release.apk "${HEADERS[@]}" "$UPLOAD_URL"
```
QA Wolf refuses a file that is not an app. A second call with the same `buildId` returns the same build.
To start runs, report the deployment with "builds": ["<buildId>"]. See Webhook.
- Tester chat — attach an
.apk,.aabor.ipato a message. The build has no environment. The runs in that chat install it, and the environment's other runs do not. - QA Wolf MCP — your coding agent calls
build_requestUpload, uploads the file, then callsbuild_create. To test the build without making it the environment's latest, the agent omitsenvironmentand passes the build id torun_createasbuilds.
Name the app by its id: the Android package name, or the iOS bundle id. The flow installs the run's build of that app:
import { launch } from "@qawolf/flows/android";
await launch({ app: { id: "com.example.app" } });This needs @qawolf/flows 0.4.0 or later. When the run has no build of that app, launch fails with "No build of <id> for <platform> on this environment". It does not fall back to another file. A flow that passes app ignores RUN_INPUT_PATH. See App resolution for Android or iOS.
Pass the build ids as builds when you create the run, to test a build without changing what the environment's other runs install. To leave the environment's latest build unchanged, upload the build with no environment. Then create the run with one of these:
- CLI —
qawolf run create --environment-id <env> --builds <build_id>. - QA Wolf MCP —
run_createwithbuilds. - HTTP —
POST https://app.qawolf.com/api/trpc/public.run.createwith{ "json": { "workspaceId": "…", "environmentId": "…", "flowIds": ["…"], "builds": ["<build_id>"] } }.
The Node.js SDK cannot create runs. Each build replaces the environment's latest build of its app and platform, for that run only. A run takes at most 20 builds.
A legacy upload puts a file in run inputs storage, and the flow reads its path from RUN_INPUT_PATH. Its runs name no expected build, so QA Wolf cannot tell whether they tested the right file. Move in three steps. Keep the legacy upload running until every flow uses app: { id }, so flows you have not changed yet still test the latest app.
- Mobile build testing — start runs on every new build.
@qawolf/ci-sdk- Webhook — every field of a deployment report.
- Legacy build uploads