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

# GitHub Deployments

> Create GitHub deployment records from GitHub Actions so QA.tech can find and test pull request previews

The [GitHub App](/configuration/github-app) tests a pull request against the preview URL for that PR's latest commit. It finds that URL through GitHub's [Deployments API](https://docs.github.com/en/rest/deployments/deployments).

Hosted platforms that connect to GitHub (Vercel, Netlify, Render, Railway, Fly.io) create these records for you. If you deploy from GitHub Actions yourself, add the steps on this page so QA.tech can wait for the preview and open the right URL.

## When you need this

Create GitHub deployments from Actions when all of these are true:

* You deploy a preview from a GitHub Actions workflow (not a GitHub-connected Vercel or Netlify integration).
* You want the GitHub App to start a review automatically, or to wait for a preview after an `@qa.tech` comment that does not include a URL.

Skip this page when any of these apply:

* Vercel, Netlify, Render, Railway, or Fly.io already posts deployment records on the PR.
* You trigger reviews with the [Change Review Action](/configuration/github-actions#change-review-action) and pass preview URLs in `applications_config`.
* You comment `@qa.tech https://preview.example.com` and want that URL used immediately.

Do not create extra deployment records on top of a platform integration that already reports success. QA.tech waits until **every** `deploy` record on the PR head commit is successful.

## What QA.tech reads

For the PR head commit (`github.event.pull_request.head.sha`), QA.tech looks up GitHub deployments with `task` set to `deploy` (the API default).

| Field             | Where you set it                                                                                              | What QA.tech uses it for                                                                                             |
| :---------------- | :------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------- |
| `ref`             | [Create a deployment](https://docs.github.com/en/rest/deployments/deployments#create-a-deployment)            | Must resolve to the PR head commit. Pass that SHA, not `github.sha` (the merge commit on `pull_request` jobs).       |
| `environment`     | Create a deployment                                                                                           | Name shown in GitHub and in **Mapped GitHub Environments**. Use a stable name per app, such as `Preview - frontend`. |
| `task`            | Create a deployment                                                                                           | Leave the default `deploy`. Other task names are ignored.                                                            |
| `state`           | [Create a deployment status](https://docs.github.com/en/rest/deployments/statuses#create-a-deployment-status) | Latest status must be `success` before a review starts.                                                              |
| `environment_url` | Create a deployment status (`success`)                                                                        | Preview URL the agent opens. GitHub's older `target_url` field is a log link, not the environment URL.               |
| `log_url`         | Create a deployment status                                                                                    | Workflow run you can open from the GitHub deployment.                                                                |

Unmapped extra environments (Storybook, docs previews) are skipped when you have **any** mapping configured. They still block readiness until their latest status is `success`.

## Create deployments from GitHub Actions

<Steps>
  <Step title="Grant workflow permissions">
    The default `GITHUB_TOKEN` can create deployments only when the job requests write access:

    ```yaml theme={null}
    permissions:
      contents: read
      deployments: write
      statuses: write
    ```

    If you extract these steps into a reusable workflow, the **caller** must grant the same permissions.

    Pull requests from forks do not get a write token on `pull_request`. Deploy those previews from a workflow in the base repository, or pass a URL with `@qa.tech` / the Change Review Action.
  </Step>

  <Step title="Create the deployment, then update its status">
    Add these steps around your existing deploy. Replace the **Deploy preview** step with your platform's command. The only requirement is that it writes `url` to `$GITHUB_OUTPUT`.

    ```yaml theme={null}
    name: Deploy preview

    on:
      pull_request:

    permissions:
      contents: read
      deployments: write
      statuses: write

    jobs:
      deploy:
        runs-on: ubuntu-latest
        env:
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          APP_NAME: frontend
        steps:
          - uses: actions/checkout@v4
            with:
              ref: ${{ env.HEAD_SHA }}

          - name: Create GitHub Deployment
            id: deployment
            uses: actions/github-script@v7.1.0
            with:
              script: |
                const headSha = process.env.HEAD_SHA;
                const appName = process.env.APP_NAME;

                const deployment = await github.rest.repos.createDeployment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  ref: headSha,
                  environment: `Preview - ${appName}`,
                  auto_merge: false,
                  required_contexts: [],
                  transient_environment: true,
                  production_environment: false,
                });
                return deployment.data.id;
              result-encoding: string

          - name: Set Deployment Status to Pending
            env:
              DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }}
            uses: actions/github-script@v7.1.0
            with:
              script: |
                const deploymentId = process.env.DEPLOYMENT_ID;
                const logUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

                await github.rest.repos.createDeploymentStatus({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  deployment_id: parseInt(deploymentId, 10),
                  state: 'pending',
                  description: 'Deploying preview...',
                  log_url: logUrl,
                });

          - name: Deploy preview
            id: vercel-deploy
            run: |
              # Replace with your deploy command. It must print a URL.
              DEPLOY_URL="https://preview.example.com"
              echo "url=$DEPLOY_URL" >> "$GITHUB_OUTPUT"

          - name: Set Deployment Status to Success
            if: success()
            env:
              DEPLOY_URL: ${{ steps.vercel-deploy.outputs.url }}
              DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }}
            uses: actions/github-script@v7.1.0
            with:
              script: |
                const url = process.env.DEPLOY_URL;
                const deploymentId = parseInt(process.env.DEPLOYMENT_ID, 10);
                const headSha = process.env.HEAD_SHA;
                const appName = process.env.APP_NAME;
                const logUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

                await github.rest.repos.createDeploymentStatus({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  deployment_id: deploymentId,
                  state: 'success',
                  environment_url: url,
                  log_url: logUrl,
                });

                await github.rest.repos.createCommitStatus({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  sha: headSha,
                  state: 'success',
                  context: `Deploy - ${appName}`,
                  description: 'Deployed successfully',
                  target_url: url,
                });

          - name: Set Deployment Status to Failure
            if: failure()
            env:
              DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }}
            uses: actions/github-script@v7.1.0
            with:
              script: |
                const deploymentId = process.env.DEPLOYMENT_ID;
                const headSha = process.env.HEAD_SHA;
                const appName = process.env.APP_NAME;
                const logUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

                if (deploymentId) {
                  await github.rest.repos.createDeploymentStatus({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    deployment_id: parseInt(deploymentId, 10),
                    state: 'failure',
                    description: 'Deployment failed',
                    log_url: logUrl,
                  });
                }

                if (headSha) {
                  await github.rest.repos.createCommitStatus({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    sha: headSha,
                    state: 'failure',
                    context: `Deploy - ${appName}`,
                    description: 'Deployment failed',
                    target_url: logUrl,
                  });
                }
    ```

    `result-encoding: string` keeps the deployment id a bare number. The default JSON encoding wraps it in quotes and `parseInt` then returns `NaN`.

    The `createCommitStatus` calls add a `Deploy - frontend` check on the PR. Context names that contain `deploy`, `deployment`, or `vercel` are extra readiness gates: if you include them, this workflow must set them to `success` or `failure`. You can omit both commit-status calls if you only need the Deployments API records.
  </Step>

  <Step title="Optional: set a Vercel alias">
    If you assign a stable hostname after `vercel deploy`, write that URL to `$GITHUB_OUTPUT` and use it as `DEPLOY_URL` on the success step:

    ```yaml theme={null}
    - name: Set alias
      id: set-alias
      if: success() && env.PREVIEW_ALIAS != ''
      env:
        PREVIEW_ALIAS: preview-${{ github.event.pull_request.number }}.example.com
      run: |
        vercel alias --scope=${{ secrets.VERCEL_TEAM_ID }} --token=${{ secrets.VERCEL_TOKEN }} set "${{ steps.vercel-deploy.outputs.url }}" "${{ env.PREVIEW_ALIAS }}"
        echo "url=https://${{ env.PREVIEW_ALIAS }}" >> "$GITHUB_OUTPUT"
    ```

    Then set `DEPLOY_URL: ${{ steps.set-alias.outputs.url || steps.vercel-deploy.outputs.url }}` on the success step so GitHub and QA.tech both receive the alias.
  </Step>
</Steps>

### Why these create-deployment flags matter

| Flag                     | Value   | Why                                                                                                                                                 |
| :----------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto_merge`             | `false` | Stops GitHub from merging the default branch into the ref before creating the deployment.                                                           |
| `required_contexts`      | `[]`    | Skips required status checks. Without an empty list, GitHub waits for checks on the commit (including **QA.tech / PR Review**), which can deadlock. |
| `transient_environment`  | `true`  | Marks the environment as a temporary preview.                                                                                                       |
| `production_environment` | `false` | Keeps GitHub from treating the preview as production.                                                                                               |

Environment protection rules (required reviewers, wait timer) still apply. If `createDeployment` returns an error, check the GitHub repository **Settings → Environments** for that name.

## Map environments to applications

After the first successful deploy, the environment name appears under **Settings → Integrations → GitHub App** in **Mapped GitHub Environments**.

<Steps>
  <Step title="Use one stable name per application">
    Names like `Preview - frontend` and `Preview - admin` map cleanly. Avoid
    per-PR names such as `pr-123`; you would have to remap them on every pull
    request.
  </Step>

  <Step title="Assign each name to a QA.tech application">
    For a project with more than one application, map each GitHub environment to
    the matching application so the review agent opens the correct preview URL.
  </Step>

  <Step title="Leave unused previews unmapped">
    Once **any** mapping exists, unmapped names (Storybook, docs) are ignored
    for testing. They must still reach `success` or the review waits.
  </Step>
</Steps>

With no mappings at all, QA.tech falls back to the project's default application. See [GitHub App setup](/configuration/github-app#how-to-set-it-up).

## Multiple applications

Run one deploy job (or one reusable workflow call) per application. Give each job its own `APP_NAME` so GitHub creates distinct environments. Then map those names in **Mapped GitHub Environments**.

If routing by environment name is a poor fit, turn off **Auto-run on PRs** and pass URLs with the [Change Review Action](/configuration/github-actions#change-review-action) instead.

## Troubleshooting

**Review never starts / "waiting for a preview deployment"**

* The deployment `ref` must be `${{ github.event.pull_request.head.sha }}`. `github.sha` on `pull_request` is the merge commit, which QA.tech does not treat as the PR head.
* Latest status must be `success`. A leftover `pending` deployment or a `Deploy - frontend` commit status blocks readiness.
* `environment_url` must be an `http://` or `https://` URL on the success status.

**Tests run against the default environment instead of the preview**

* Confirm the success status includes `environment_url`.
* Map the GitHub environment name in **Mapped GitHub Environments**.
* If a mapping exists but this name is missing from it, QA.tech skips that deployment on purpose.

**`createDeployment` fails or never returns an id**

* Set `required_contexts: []`.
* Check environment protection rules on the GitHub repository **Settings → Environments**.
* Confirm `permissions.deployments: write` on the job (and on the reusable-workflow caller).

**Several apps, only one URL tested**

* Create one GitHub environment per application and map each name.
* Or pass per-application URLs with the [Change Review Action](/configuration/github-actions#change-review-action).

## Related documentation

* **[GitHub App](/configuration/github-app)** - Automatic PR reviews that consume these deployment records
* **[GitHub Actions](/configuration/github-actions)** - Test Run Action and Change Review Action
* **[Preview Environments](/core-concepts/applications-and-environments#preview-environments)** - How QA.tech stores per-PR URLs
* **[Vercel Preview Protection](/configuration/vercel-preview-protection)** - Bypass headers when the preview is locked
