# GitHub Actions: Deploy and test

Run your test suite against the deployed environment to validate changes before
considering them complete.

```yaml title=".github/workflows/deploy-and-test.yaml"
name: Deploy and Test

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  deploy-and-test:
    runs-on: ubuntu-latest
    env:
      ZUPLO_API_KEY: ${{ secrets.ZUPLO_API_KEY }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install dependencies
        run: npm install

      - name: Deploy to Zuplo
        id: deploy
        shell: bash
        env:
          # head_ref is the source branch on pull_request events (where the
          # checkout is the pull/<number>/merge ref, not the branch);
          # ref_name covers push events
          ENVIRONMENT: ${{ github.head_ref || github.ref_name }}
        run: |
          # Capture deployment output
          OUTPUT=$(npx zuplo deploy --api-key "$ZUPLO_API_KEY" --environment "$ENVIRONMENT" 2>&1)
          echo "$OUTPUT"

          # Extract the deployment URL
          DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oP 'Deployed to \K(https://[^ ]+)')
          echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT

      - name: Run tests
        run: npx zuplo test --endpoint "${{ steps.deploy.outputs.url }}"
```

This workflow:

1. Deploys to Zuplo and captures the deployment URL.
2. Runs your test suite against the live deployment.
3. Fails the workflow if any tests fail.

The deploy step passes `--environment` explicitly because this workflow runs on
both `push` and `pull_request` events. The expression
`${{ github.head_ref || github.ref_name }}` resolves to the branch name on
either trigger, so every run for a branch updates the same environment. Without
it, `pull_request` runs create a second environment named after the PR merge ref
instead of your branch. For details, see
[PR preview environments](./pr-preview-environments.mdx).

## Write tests

`describe`, `it`, and `TestHelper` come from `@zuplo/test`, which a Zuplo
project already has through the `zuplo` package. Assertions use
[chai](https://www.chaijs.com/api/bdd/), which a new project does **not**
include. Install it first:

```bash
npm install --save-dev chai @types/chai
```

Place test files in the `tests` folder with the `.test.ts` extension:

```typescript title="tests/api.test.ts"
import { describe, it, TestHelper } from "@zuplo/test";
import { expect } from "chai";

describe("API", () => {
  it("returns 200 for health check", async () => {
    const response = await fetch(`${TestHelper.TEST_URL}/health`);
    expect(response.status).to.equal(200);
  });

  it("requires authentication", async () => {
    const response = await fetch(`${TestHelper.TEST_URL}/protected`);
    expect(response.status).to.equal(401);
  });
});
```

`TestHelper.TEST_URL` is the value passed to `--endpoint`, so the same files run
against every environment. For the full setup, see
[Get started with zuplo test](../testing-getting-started.mdx).

## Next steps

- Add [PR preview environments](./pr-preview-environments.mdx) with automatic
  cleanup
- Run [local tests](./local-testing.mdx) before deploying
