> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qawolf.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Legacy build uploads

> Upload an app file to run inputs storage, report it as a deployment with RUN_INPUT_PATH, and let a trigger run your flows.

> [!NOTE]
>
> A workspace with builds turned on uploads Android and iOS builds as described in [Upload builds](https://docs.qawolf.com/upload-builds.md), and its runs record which build they tested. This page covers the legacy path, which Windows and Linux app files still take.

Your pipeline uploads each new app file to run inputs storage and reports it as a deployment, and a [trigger](https://docs.qawolf.com/triggers/overview.md) you create starts the runs. The flow reads the file's path from `RUN_INPUT_PATH`.

## How it works

**1. Upload the build**

Your pipeline sends the build to QA Wolf storage and gets back the path it is stored under.

**2. Report the deployment**

Your pipeline calls `deployment.reportStatus`, carrying the build's path on the runner as the `RUN_INPUT_PATH` environment variable.

**3. A trigger matches**

The deployment's first `success` report evaluates your triggers. Each one that matches creates a run, and the run carries `RUN_INPUT_PATH`.

**4. The flow launches the build**

`launch` falls back to `RUN_INPUT_PATH` when the flow names no app, so the run executes against the build that was just uploaded.

> [!WARNING]
>
> Uploading and reporting start nothing on their own. A workspace with no matching trigger records the deployment, runs nothing, and the pipeline step still succeeds. Create the trigger before you wire up the pipeline.

## Requirements

* An API key, created under **Workspace Settings → API Keys**. A workspace, organization or user key all work.
* The workspace id, shown on the same page.
* A QA Wolf environment to report into. The deploy resolves to an environment by alias or name, and a name that matches nothing creates a new environment that no trigger targets.
* One of: the [QA Wolf Fastlane plugin](https://github.com/qawolf/fastlane-plugin-qawolf) 1.0.0 or later, `qawolf/notify-qawolf-on-deploy-action@v3`, or `@qawolf/ci-sdk` 3.1.0 or later. Earlier versions report through the legacy deploy webhook, which [triggers](https://docs.qawolf.com/triggers/overview.md) never see. See [Mobile build testing with legacy triggers](https://docs.qawolf.com/legacy-triggers/mobile-build-testing.md).

## Name the build file

You choose the name the build is stored under, and your flows resolve the build by that name. Use one stable name per environment and platform, so each upload replaces the last:

```text theme={null}
app-staging-android.apk
app-staging-ios.ipa
```

This path accepts app builds only. [v0/run-inputs-executables-signed-urls](https://docs.qawolf.com/run-inputs-executables-signed-urls.md) lists the extensions it takes; a file your flows read goes to [team storage](https://docs.qawolf.com/use-files-in-flows.md#upload-a-file) instead.

## The path the run needs

Uploaded builds are mounted on the runner under `/home/wolf/run-inputs-executables/`. `RUN_INPUT_PATH` has to carry the **absolute** path:

```text theme={null}
/home/wolf/run-inputs-executables/app-staging-android.apk
```

> [!WARNING]
>
> A bare filename does not resolve on an Android or iOS runner. The flow passes `RUN_INPUT_PATH` to the device as given, so a relative path fails to find the build. The Fastlane plugin builds the absolute path for you; on every other path you prepend the directory yourself.

## Set up the pipeline

> [tab] Fastlane
>
> The plugin is not published to RubyGems, so point the `Gemfile` at the repository and pin a tag.
>
> ```ruby theme={null}
> gem "fastlane-plugin-qawolf", git: "https://github.com/qawolf/fastlane-plugin-qawolf", tag: "1.0.0"
> ```
>
> Run `fastlane install_plugins`, then add both actions to the lane that builds your app. `upload_to_qawolf` stores the build and leaves its filename in the lane context; `notify_deploy_qawolf` reports the deployment and sets `RUN_INPUT_PATH` to that build for you.
>
> ```ruby expandable theme={null}
> lane :qawolf do
>   # Your existing build step, gradle for Android or build_app for iOS
>
>   upload_to_qawolf(
>     # Also read from QAWOLF_API_KEY
>     qawolf_api_key: ENV.fetch("QAWOLF_API_KEY", nil),
>     # No extension — it is taken from the build you produced
>     executable_file_basename: "app-staging-android"
>   )
>
>   notify_deploy_qawolf(
>     qawolf_api_key: ENV.fetch("QAWOLF_API_KEY", nil),
>     workspace_id: ENV.fetch("QAWOLF_WORKSPACE_ID", nil),
>     # The name or an alias of an environment that already exists
>     environment: "Staging",
>     # Which build this is, matched by a trigger's servicePattern
>     service: "android"
>   )
> end
> ```
>
> `notify_deploy_qawolf` reports a `success` status by default, which is the status that evaluates triggers. It sets `QAWOLF_DEPLOYMENT_ID` and `QAWOLF_ENVIRONMENT_RUNS_URL` in the lane context and the environment, and returns the deployment id. Runs start asynchronously afterwards, so no run id comes back.
>
> > [!WARNING]
> >
> > A matrix over Android and iOS derives the same deployment identity for both legs, because no CI system exposes which leg is running. Reporting into one environment, the second leg starts nothing and the lane still goes green. Pass `provider_deployment_discriminator` on every job that deploys more than once at a time:
> >
> > ```ruby theme={null}
> > notify_deploy_qawolf(
> >   environment: "Staging",
> >   service: ENV.fetch("PLATFORM", nil),
> >   provider_deployment_discriminator: ENV.fetch("PLATFORM", nil)
> > )
> > ```
>
> The plugin's [README](https://github.com/qawolf/fastlane-plugin-qawolf#notify_deploy_qawolf-options) lists every option, including `repository` and `pull_request_number` for PR testing, and the upgrade steps from 0.x.

> [tab] GitHub Actions
>
> Upload the build, then report the deployment with the upload step's output, under the runner's executables directory, as `RUN_INPUT_PATH`.
>
> ```yaml expandable theme={null}
> - name: Upload the build to QA Wolf
>   id: upload
>   uses: qawolf/upload-run-inputs-executable-action@v2
>   with:
>     qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
>     # Stored under this file's own name, so name the build file deliberately
>     input-file-path: ./path/to/app-staging-android.apk
>
> - 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
>     variables: >
>       { "RUN_INPUT_PATH": "/home/wolf/run-inputs-executables/${{ steps.upload.outputs.destination-file-path }}" }
> ```
>
> The upload action stores the build under the basename of `input-file-path` and outputs that path as `destination-file-path`. It takes no workspace id, so it needs a workspace API key.
>
> A pipeline that ships an Android and an iOS build into one environment sets `service` on each report, from version 3.1.0 of the action, so a trigger's `servicePattern` can tell them apart.
>
> > [!WARNING]
> >
> > The action derives one deployment per workflow run, attempt, job and environment. Two legs of one matrix deploying the same environment therefore share a deployment, and only the first starts runs. Set `deployment-id` per leg to separate them.
>
> `environment` names an existing QA Wolf environment. Omit it on a pull request event and the action reports into an ephemeral environment named after the repository and the pull request number. See [PR testing](https://docs.qawolf.com/deployment-testing/pr-testing.md).

> [tab] Node.js
>
> For a CI system that runs Node.js but is neither Fastlane nor GitHub Actions. Requires Node.js 24.14.1 or later, below 25.
>
> ```bash theme={null}
> npm install @qawolf/ci-sdk
> ```
>
> ```javascript expandable theme={null}
> import assert from "assert";
> import fs from "fs/promises";
> import {
>   detectProviderDeploymentId,
>   makeQaWolfSdk,
> } from "@qawolf/ci-sdk";
>
> const apiKey = process.env.QAWOLF_API_KEY;
> assert(apiKey, "QAWOLF_API_KEY is required");
>
> const workspaceId = process.env.QAWOLF_WORKSPACE_ID;
> assert(workspaceId, "QAWOLF_WORKSPACE_ID is required");
>
> const { deployment, generateSignedUrlForRunInputsExecutablesStorage } =
>   makeQaWolfSdk({ apiKey });
>
> const signed = await generateSignedUrlForRunInputsExecutablesStorage({
>   destinationFilePath: "app-staging-android.apk",
>   workspaceId,
> });
> if (!signed.success || !signed.uploadUrl || !signed.interactiveRunFileLocation) {
>   throw new Error(`Could not get an upload URL: ${JSON.stringify(signed)}`);
> }
>
> const uploaded = await fetch(signed.uploadUrl, {
>   method: "PUT",
>   body: await fs.readFile("./path/to/app-staging-android.apk"),
>   headers: { "Content-Type": "application/octet-stream" },
> });
> if (!uploaded.ok) throw new Error(`Upload failed: ${uploaded.status}`);
>
> const detected = detectProviderDeploymentId({ discriminator: "android" });
> if (detected.outcome !== "detected") {
>   throw new Error(`Could not identify this deployment: ${JSON.stringify(detected)}`);
> }
>
> const report = await deployment.reportStatus({
>   workspaceId,
>   providerDeploymentId: detected.id,
>   status: "success",
>   environment: { name: "staging" },
>   service: "android",
>   environmentVariables: {
>     RUN_INPUT_PATH: `/home/wolf/run-inputs-executables/${signed.interactiveRunFileLocation}`,
>   },
> });
> if (report.outcome !== "reported") {
>   throw new Error(`Failed to report the deployment: ${JSON.stringify(report)}`);
> }
> console.log(`Reported deployment ${report.deployment.id}: ${report.deployment.url}`);
> ```
>
> * `discriminator` separates the deployments one CI job makes at the same time, such as the legs of an Android and iOS matrix. Without it both legs derive the same identity, and only the first starts runs.
> * `environmentVariables` replaces the values a previous report of the same deployment stored, rather than merging into them.
> * To block the pipeline on the result, add `deployment.waitForVerdict`. See [Node.js SDK](https://docs.qawolf.com/deployment-testing/ci-sdk.md).

> [tab] Webhook
>
> For a pipeline that cannot run Node.js — ArgoCD, locked-down runners, minimal containers. Requires only HTTP requests.
>
> **1. Generate a signed URL and upload the build**
>
> ```bash theme={null}
> curl "https://app.qawolf.com/api/v0/run-inputs-executables-signed-urls?file=app-staging-android.apk" \
>   -H "Authorization: Bearer $QAWOLF_API_KEY"
> ```
>
> On success you get back:
>
> ```json theme={null}
> {
>   "fileLocation": "$WORKSPACE_ID/app-staging-android.apk",
>   "interactiveRunFileLocation": "app-staging-android.apk",
>   "signedUrl": "https://..."
> }
> ```
>
> ```bash theme={null}
> curl -X PUT \
>   --header "Content-Type: application/octet-stream" \
>   --data-binary @./path/to/app-staging-android.apk \
>   "$SIGNED_URL"
> ```
>
> An organization or user API key also needs `workspaceId` on the first request. See [v0/run-inputs-executables-signed-urls](https://docs.qawolf.com/run-inputs-executables-signed-urls.md).
>
> **2. Report the deployment**
>
> Carry `interactiveRunFileLocation`, under the runner's executables directory, as `RUN_INPUT_PATH`.
>
> ```bash expandable theme={null}
> curl -X POST "https://app.qawolf.com/api/trpc/public.deployment.reportStatus" \
>   -H "Authorization: Bearer $QAWOLF_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d "{
>     \"json\": {
>       \"workspaceId\": \"$QAWOLF_WORKSPACE_ID\",
>       \"providerDeploymentId\": \"$BUILD_ID-android\",
>       \"status\": \"success\",
>       \"environment\": { \"name\": \"staging\" },
>       \"service\": \"android\",
>       \"environmentVariables\": { \"RUN_INPUT_PATH\": \"/home/wolf/run-inputs-executables/app-staging-android.apk\" }
>     }
>   }"
> ```
>
> `providerDeploymentId` is whatever your pipeline calls this build. It has to differ for every build, and for each leg of an Android and iOS matrix, because only a deployment's first `success` report evaluates triggers. See [Webhook](https://docs.qawolf.com/deployment-testing/webhook.md) for every field and for the response shape.

> [!NOTE]
>
> These examples upload an Android `.apk`. iOS works identically — point the same steps at your `.ipa`, and name the service `ios`.

## Create a trigger

A deployment starts nothing until a trigger matches it. Create one on the workspace's triggers page, with conditions that match the deployments you now report:

| Condition | For a mobile pipeline |
| - | - |
| `environmentIds` or `environmentPattern` | The environment the build reports into. |
| `servicePattern` | `android` or `ios`, when both platforms report into one environment. A deployment that names no service never matches a trigger carrying a service pattern. |
| `branchPattern` | Narrow to the branch the build came from, when you report `metadata.ref`. |

Shipping both platforms into one environment needs two triggers, one per `servicePattern`, each selecting that platform's flows. Without the service condition both triggers fire on both builds, and the Android flows run against the iOS build.

`deployTargetPattern` matches on the deploy target URL, which a mobile build has none of, so it is not a way to tell builds apart.

See [Trigger fields](https://docs.qawolf.com/triggers/reference.md) for every condition and for the action that selects which flows run.

> [!TIP]
>
> You can create and manage triggers from your own coding agent instead of the triggers page. Add the [QA Wolf MCP](https://docs.qawolf.com/qawolf-mcp.md) and ask it to set up the triggers for your mobile environments — it can also list recent deployments and show why a trigger did or did not fire.

## Use the build in a flow

A flow that names no app launches whatever `RUN_INPUT_PATH` points at, so the run uses the build that was just uploaded:

```ts theme={null}
await launch({ appPackage: "com.example.app" });
```

Naming `app` at all stops the fallback: `launch({ app: ... })` resolves `app.path`, then `app.env`, then `app.url`, then `app.id`, and never falls back to `RUN_INPUT_PATH`. On Android and iOS these paths must be absolute too. See App resolution for [Android](https://docs.qawolf.com/libraries/flows/api-reference/android.md#app-resolution) or [iOS](https://docs.qawolf.com/libraries/flows/api-reference/ios.md#app-resolution).

## Verify the integration

**1. Run the pipeline**

Run the CI job that builds your app.

**2. Check the upload and the report**

The upload step succeeds, and the report step logs the deployment id and the environment's runs page.

**3. Check the trigger fired**

Open the deployment in QA Wolf. It lists a verdict for every trigger, including the reason a trigger did not match. See [Deployments](https://docs.qawolf.com/triggers/deployments.md#evaluation).

**4. Check the run used the build**

Open the run the trigger created and confirm it launched the build you just uploaded.

## Troubleshooting

* **The build uploads, the deployment is recorded, and nothing runs** — no trigger matched. Open the deployment and read the per-trigger verdicts. The usual causes are an environment name that created a new environment instead of resolving an existing one, and a `servicePattern` on the trigger that the report does not send a `service` for.
* **Only one of the two platforms runs** — both legs reported under the same `providerDeploymentId`, so the second updated the first deployment instead of creating one, and only a deployment's first `success` report evaluates triggers. Give each leg its own identity: `provider_deployment_discriminator` on Fastlane, `discriminator` on `detectProviderDeploymentId`, `deployment-id` on the GitHub Action.
* **The Android flows ran against the iOS build** — the triggers have no `servicePattern`, so both fired on both deployments.
* **The flow can't find the build** — most often `RUN_INPUT_PATH` is a bare filename rather than an absolute path under `/home/wolf/run-inputs-executables/`. Otherwise check that the report carried `environmentVariables`, that the name matches what was uploaded, and that a later report of the same deployment did not replace the variables — a report that sends `environmentVariables` replaces the stored map rather than merging into it.
* **The pipeline step passes but the workspace never sees the deploy** — the step is still on the legacy path. `@v1` and `@v2` of the GitHub Action, Fastlane plugin 0.4.8 and below, and the SDK's `attemptNotifyDeploy` all report through the legacy deploy webhook, which triggers never see.
* **Authentication errors** — check `QAWOLF_API_KEY`, and that `workspaceId` names the workspace the key may act on.

## Related

* [Triggers](https://docs.qawolf.com/triggers/overview.md) and [Trigger fields](https://docs.qawolf.com/triggers/reference.md)
* [Deployments](https://docs.qawolf.com/triggers/deployments.md) — how a deployment resolves to an environment, and the verdict per trigger.
* [v0/run-inputs-executables-signed-urls](https://docs.qawolf.com/run-inputs-executables-signed-urls.md)
* [`@qawolf/ci-sdk`](https://docs.qawolf.com/libraries/ci-sdk/api-reference)
* [Mobile build testing with legacy triggers](https://docs.qawolf.com/legacy-triggers/mobile-build-testing.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.