Tutorial

Connect a CI job

Send test results from a CI job with the TestLookup command-line tool, wait until TestLookup has saved them, and fail the build only when tests really fail.

Client reference

Prerequisites

This tutorial connects a continuous integration (CI) job to TestLookup. CI is the service that builds and tests your code on every change, such as GitHub Actions or Jenkins. You need:

This tutorial sends a finished report after the tests have run. To send results live while the tests run, you use separate reporters, which follow their own session rules. See the client reference.

Client choice

This tutorial uses the TestLookup command-line tool, called testlookup. TestLookup also has a Python reporter and clients for JavaScript, Java and Go, and they offer different commands.

Before you swap in another client, check the client reference. Not every client supports every command for uploads, live results, progress checks or pass and fail decisions. The integration map on GitHub lists what each one does.

The command-line tool, the code libraries (SDKs) and the MCP server aren’t published to a package registry. The MCP server lets AI assistants use TestLookup. Install the command-line tool, version 0.4.0, from a copy of the code, as its README on GitHub describes:

Terminal window
git clone https://github.com/test-intelligence/testlookup.git
python -m pip install ./testlookup/cli
testlookup --version

To stay on one exact version, check out a specific commit in your copy before you install. Then follow the README from that same commit.

If you’d rather send results live, use a reporter. Reporters exist for pytest, Robot Framework, Jest, Mocha, Playwright, Vitest, Cypress, Cucumber-JS, TestNG, JUnit 5 and Go’s built-in testing package. The Cypress reporter sends results for one spec file at a time.

Store the secrets

Store three values in your CI system’s protected secrets:

SecretValue
TESTLOOKUP_URLThe server’s API address, for example https://testlookup.example.com
TESTLOOKUP_API_KEYThe key
TESTLOOKUP_PROJECT_IDThe project ID

The command-line tool reads the address and the key from the first two values. You pass the project ID yourself with an option.

Details: the tool sends the key in the X-API-Key header. You pass the project ID with --project.

Keep the key secret:

Use a key that’s bound to the project and has a filled-in list of permissions, called scopes. A list with entries limits what the key can do.

Older keys with an empty list can have wider access, so never treat them as having no access. Replace or review them before you use them in CI.

The key’s owner needs the QA engineer role or higher to send results.

Keep retries in one run

Give each submission a fixed identity. Use the build number, the commit ID, the branch and, where you can, the link to the CI run.

The tool picks up some of these details by itself from GitHub Actions, GitLab CI, Jenkins, Azure DevOps and CircleCI. It detects the CI system, the repository, the pull request, who started the job and the link to the run.

It can’t detect the build number, commit ID or branch, so pass those yourself. Anything you pass replaces what the tool detects.

When a job retries the same submission, reuse the same build number. Then every attempt ends up in the same run, instead of creating look-alike runs.

Use the commit ID, not only the branch name. Don’t make a new build number for each network retry.

By default, the upload also sends details about where the tests ran:

One option turns those details off, and another skips the AI analysis of the run.

Details: pass the build number with --build, which is required, the commit with --commit and the branch with --branch. --no-run-context leaves out the extra details, and --no-ai skips the AI analysis.

Upload the report

After your tests have written their report, upload it and keep the run ID that comes back:

Terminal window
RUN_ID=$(testlookup upload file results.xml \
--project "$TESTLOOKUP_PROJECT_ID" \
--build "$BUILD_ID" \
--commit "$COMMIT_SHA" \
--wait -o json | jq -r '.run_id')

Fill in the build ID and the commit ID from your CI system’s own variables. The accepted upload also returns a task ID. Keep both IDs in the job’s log or details.

The workflow guide on GitHub explains why. An accepted upload starts work in the background. It isn’t a pass or fail decision.

While you set this up, use a report with made-up results. Stay within the format and upload limits. Let the job fail clearly when the tool or the server refuses the report.

Wait for the results

With the wait option, the tool keeps checking the upload until the server has saved the report. The command fails if the server refuses the report, for example because the file is empty or can’t be read. By default, it waits up to 600 seconds.

Without the wait option, the command returns as soon as TestLookup accepts the upload.

Details: --wait turns waiting on, and --wait-timeout sets the time limit in seconds. A refusal ends the command with a non-zero exit code.

TestLookup can save a run before its analysis or report is finished. If a later step depends on the analysis or the review, check for that too, with a time limit.

Report each problem as what it is: a failed task, rejected results, skipped analysis or a pending review. Don’t lump them together as one general job failure.

Pass or fail the job

Then judge the run:

Terminal window
testlookup ci-verdict --run "$RUN_ID" --project "$TESTLOOKUP_PROJECT_ID"

The ci-verdict command looks at the run’s failed and broken tests, and at the project’s quarantine list. Quarantine sets a flaky test aside so its failures don’t block the build. A flaky test is one that sometimes passes and sometimes fails with no change to the code.

The command ends with an exit code, a number that your CI system reads. 0 means OK, and anything else fails the job:

Exit codeMeaning
0No real failures. Quarantined failures don’t block.
1At least one real failure that isn’t quarantined, or any failure at all with --strict.
2An API or network error, or the run was still in progress when --timeout-seconds ran out.

Exit code 2 fails the job unless you add --fail-open.

By default, TestLookup proposes flaky tests for quarantine automatically. But nothing is quarantined until a QA lead approves a proposal. So at first, every failure counts as real.

The ci-verdict command isn’t a release decision, and it doesn’t replace report review. To pass or fail a job on a release, use the release gate command. It reads the release’s saved decision:

Terminal window
testlookup release gate <release>
Exit codeMeaning
0GO, or Conditional GO unless you pass --fail-on CONDITIONAL_GO
1NO-GO
3No decision to act on: Not evaluated, or never evaluated
2An infrastructure error, or a release name that’s unknown or matches more than one release

The table includes Conditional GO because the command can handle it. At this version, though, TestLookup gives a whole release only GO, NO-GO or Not evaluated.

Exit codes differ between commands, so check each command’s own table.

Details: --evaluate records a fresh decision first, and needs a QA lead or a project API key. --json prints the release ID and the gate result.

Here’s a minimal GitHub Actions step. Place it after the steps that install the tool and run your tests:

- name: Send results to TestLookup
env:
TESTLOOKUP_URL: ${{ secrets.TESTLOOKUP_URL }}
TESTLOOKUP_API_KEY: ${{ secrets.TESTLOOKUP_API_KEY }}
TESTLOOKUP_PROJECT_ID: ${{ secrets.TESTLOOKUP_PROJECT_ID }}
run: |
RUN_ID=$(testlookup upload file results.xml --project "$TESTLOOKUP_PROJECT_ID" \
--build "${{ github.run_id }}" --commit "${{ github.sha }}" --wait -o json | jq -r '.run_id')
testlookup ci-verdict --run "$RUN_ID" --project "$TESTLOOKUP_PROJECT_ID"

For each result, write down which run, which quarantine state and which version of the tool produced it.

Retry limits

The tool’s upload already retries when the server is busy or has hit a limit (HTTP 429 or 503). It waits for the time the server gives in its Retry-After header.

If there isn’t one, it starts with half a second and waits longer each time, for up to five minutes in total. If you write your own client, do the same.

A fixed build identity doesn’t make every other request that sends data safe to repeat.

If TestLookup couldn’t read the file or refused your permissions, fix the cause before you retry. Keep a log of each attempt, without the key.

Cleanup

When you finish the tutorial, collect the run ID, the task ID, the result and any failure details. Then:

  1. Revoke the temporary key.
  2. Remove the test-only CI secrets and build output files.
  3. Delete the test project if you no longer need it.

What a recorded run showed

This run was recorded on September 21, 2026, with an earlier development version of TestLookup, before the release gate command existed. Details may differ on later versions.

The tool ran in a throwaway container against a throwaway TestLookup on the same machine, not on a hosted CI service. The upload named the TestLookup repository, a commit and a build named after that commit. Checking the upload ended with 2 results accepted, 0 rejected, 1 passed and 1 failed.

The upload command succeeded, with exit code 0, because TestLookup saved the results. Then ci-verdict returned fail, with exit code 1: one real failure, none quarantined. When you adapt the workflow, write down your own CI system and machine details.

Read the client reference.