GitHub Actions
Run API requests, flows and load tests in a GitHub workflow. There is a published action, so a step is three lines of YAML and nothing needs installing first.
Atrahasis CI Action on GitHub MarketplaceThe shortest thing that works
Drop this into .github/workflows/api-tests.yml. It calls a real endpoint and fails the job if the answer is wrong.
name: API tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Health check
uses: atrahasisdev/action@v1
with:
args: GET https://api.example.com/health -a "status eq 200"No actions/checkout is needed for a single request, because there is no file to read. You only need a checkout once you start running flows or load specs.
What the action takes
| Input | Required | What it is |
|---|---|---|
args | Yes | Everything you would type after atra, passed through verbatim. |
working-directory | No, defaults to . | The folder to run in. Only needed when your flow or spec group sits somewhere other than the repository root. |
The action is a thin composite wrapper: it runs npx --yes @atrahasis/cli with your arguments on the runner. GitHub-hosted runners ship with Node already, so there is no setup step and no version to keep current.
args string is handed to bash, so quote each assertion the way you would in a terminal: -a "status eq 200". atra reassembles an unquoted assertion correctly, so -a status eq 200 also works. Quoting still matters because of what the shell does on the way: an unquoted value is subject to variable expansion and filename globbing before atra ever sees it.Passing a base URL or a credential
Flows and load specs get their values from an Atrahasis environment, and a variable marked as a secret with its source set to OS is read from an operating system environment variable named exactly like the key. So a key of base_url needs an environment variable called base_url.
- name: Run flow
uses: atrahasisdev/action@v1
env:
base_url: ${{ vars.BASE_URL }}
api_token: ${{ secrets.API_TOKEN }}
with:
args: run -f logout -e devThe GitHub side can be called anything you like, here BASE_URL and API_TOKEN. What matters is the name on the left of the env: mapping, because that is the name atra looks up. Nothing is uppercased or prefixed along the way, and Linux runners are case-sensitive.
Which one, secrets or vars?
GitHub stores these separately and treats them differently. Pick by whether the value would do any harm in a public build log.
| Use | For | In logs |
|---|---|---|
${{ vars.NAME }} | Configuration that is not sensitive: a base URL, a tenant name, a feature flag, a timeout. | Shown in plain text |
${{ secrets.NAME }} | Anything that grants access: tokens, passwords, API keys, client secrets. | Masked automatically |
A base URL is usually configuration rather than a credential, which is why the example above reads it from vars and keeps secrets for the token. Use secrets for the URL too if your staging hostname is not something you want in a public log.
vars still has to be marked as a secret with source OS in the app, or atra will never look for it.Where to put them in GitHub
Both live in the same place, on separate tabs:
The Secrets tab feeds secrets.* and the Variables tab feeds vars.*. A secret can be read back only by a workflow, never by you: if you forget the value you replace it rather than view it. A variable can be read and edited freely.
Both tabs offer three scopes. Repository is the usual choice and applies to every workflow in the repository. Environment scopes a value to a named GitHub Environment and can sit behind a required reviewer, which is how teams keep a production credential away from pull request builds. Organization shares one value across many repositories, useful when the same base URL is tested by several services.
staging does not select the Atrahasis environment named staging. They are unrelated systems. You always pass -e yourself.Getting your flows and specs onto the runner
Flows and load specs are built in Atrahasis, which writes them as plain files. Commit that folder with the rest of your code and a plain checkout brings it along, tests included.
- uses: actions/checkout@v4
- uses: atrahasisdev/action@v1
env:
base_url: ${{ vars.BASE_URL }}
with:
args: run -f logout -e devThat is the whole thing. No token to arrange, because a workflow can always read its own repository, and no working-directory, because atra runs from the root where the group already sits.
Where atra runs matters
atra reads the flow group from the folder it is run in. With the group at the repository root there is nothing to say. If several groups live side by side, name the one you want as the first argument: run checkout-flows -f signup -e dev. If they sit under a subfolder, point working-directory at it.
The three jobs, one at a time
1. A request with assertions
Best after a deploy, to prove the thing answered. Every assertion has to pass or the step fails.
- name: Health check
uses: atrahasisdev/action@v1
with:
args: >-
GET https://api.example.com/health
-a "status eq 200"
-a "$.status eq UP"
-a "response_time lt 2000"The >- folds a long command over several lines without introducing newlines, which keeps a wide set of assertions readable in review.
2. A flow from the app
A flow chains requests and passes values between them, such as logging in and reusing the token. Drop -f to run every flow in the group.
- name: Run flow
uses: atrahasisdev/action@v1
env:
base_url: ${{ vars.BASE_URL }}
with:
args: run -f logout -e dev3. A load test from the app
Same spec you built in the app, run headless. Pick the profile with -t, and drop -s to run every spec in the group.
- name: Load test
uses: atrahasisdev/action@v1
env:
base_url: ${{ vars.BASE_URL }}
with:
args: run -s logout -t load -e devLoad tests are long and noisy compared to the rest of a pipeline. A common arrangement is to keep requests and flows on every push, and put the load test on a schedule or behind workflow_dispatch.
A complete workflow
All three jobs together, with the test repository checked out and the secret bridged. Copy it and change the names.
name: API tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
api-tests:
runs-on: ubuntu-latest
env:
base_url: ${{ vars.BASE_URL }}
steps:
- uses: actions/checkout@v4
- name: Health check
uses: atrahasisdev/action@v1
with:
args: >-
GET ${{ vars.BASE_URL }}/health
-a "status eq 200"
-a "$.status eq UP"
- name: Run flows
uses: atrahasisdev/action@v1
with:
args: run -e dev
- name: Load test
if: github.event_name == 'workflow_dispatch'
uses: atrahasisdev/action@v1
with:
args: run -s logout -t load -e devTwo details worth copying. The job-level env: sets base_url once for every step rather than repeating it. And run -e dev with no -f runs every flow in the group, so adding a flow in the app needs no change here.
How the job turns red
atra exits 0 when every check passed and non-zero when any of them did not, which is exactly what GitHub Actions looks at. A failed assertion in a request, a failed step in a flow and a breached threshold in a load test all fail the step and stop the job. You do not need a wrapper script or any parsing of the output.
If you want the remaining steps to run anyway, that is the usual continue-on-error: true on the step. Reach for it sparingly: a step that cannot fail the build is a step nobody reads.