This page is for pytest tests in a project backed by a repository. To send report files that aren’t tied to a commit, see Send results from your harness.
Before you start
- A project backed by a repository. See Connect a repository.
- An API key that names the project. Create a key, choose Only these projects, and pick this project. A key created with All web projects, including ones created later can’t send lab results: Peeps answers
404 not_found_or_no_access, as if the project didn’t exist. - Keep the key in your lab’s or CI system’s secret store. The examples below read it from
PEEPS_API_KEY.
Send a lab run
Send onePOST request per lab run to /api/v1/projects/{project}/ci-results, with a JSON body. {project} is the project’s acronym, in any case, or its ID.
LAB with your project’s acronym. --data-binary sends the body with its size, which Peeps requires. Peeps answers 201 once the run is filed:
filed counts the tests Peeps filed. unmatched lists tests in the JUnit that the project has no test case for: an upload never creates a test case, so these aren’t filed.
See Send lab results in the API reference for every field, response and error.
What Peeps checks
A CI job proves which repository and commit it ran with its identity token. A key can’t, so Peeps checks the upload against your repository instead:- The branch.
gitRefmust be the headline branch or a branch listed under Tracked branches. Otherwise Peeps answers422 branch_not_followed. - The commit.
commitShamust be on that branch: its latest commit or an earlier one. Otherwise Peeps answers422 unknown_commit. - The repository. The project must still be connected to its repository. Otherwise Peeps answers
422 not_connected.
What Peeps files
- One batch per upload, named after the camera model, the firmware and the commit, such as Lab CD62 · v2026.10.27.2 @ 1a2b3c4. Its runs count as CI runs, and failure analysis follows as it does for a run your CI reports.
- One run per test function and browser. Each parametrization, such as a camera profile, is a result of that one test case. A parameter that pytest retried counts by its last attempt.
- The lab’s context comes first on each result, so you can see which camera, firmware and machine it ran on.
- Sending again files a second batch. Peeps doesn’t recognize a resend, so send each lab run once.
- A lab run never starts an automatic fix. A result sent with a key doesn’t send Peeps into your repository on its own. You can still click Heal in your repository on a failed run. See Fix a failing test.
Limits
Verify fixes on the lab
When Peeps fixes a pytest test, it runs the test through your.peeps/run-tests hook if your repository has one. A lab run often takes longer than a fix can wait: reserving a device alone can take half an hour. So the hook can start the lab run and return without a result. Peeps then opens the fix, and the lab’s upload for the fix decides it.
Lab-verified fixes work for pytest tests, and need the latest version of the Peeps action.
Return from the hook without a result
To say the lab will report later, the hook must:- exit with
0 - write no JUnit file
- print, as its last line, exactly
peeps:pending, optionally followed by a space and a note, such as a link to the lab run
peeps/… branch and reads your repository’s workflows at that commit, to see whether one will run the lab on that push.
A fix into another pull request’s branch can’t wait for the lab, so when the hook prints the marker, Peeps opens no pull request for it.
When a workflow runs the lab on the fix’s branch
A workflow under.github/workflows/ counts when, at the fix’s commit:
- it runs on a push to the fix’s branch. Peeps follows GitHub’s
branches,branches-ignore,tags,pathsandpaths-ignorefilters, matchingpathsagainst the files the fix changed. It doesn’t read a job’s ownif:. - its file contains
/ci-results, the path of the lab results endpoint.
- A pass marks the pull request ready for review: “The lab’s results for
peeps/fix-…at1a2b3c4reporttest_capturepassed, so Peeps marked this pull request ready for review.” - A failure closes it without merging: “The lab’s results for
peeps/fix-…at1a2b3c4reporttest_capturefailed, so Peeps closed this pull request without merging.” The comment quotes the failure’s first line. - Skips alone settle nothing, and the draft keeps waiting.
jobUrl as Lab run:. Peeps masks your project’s secrets in the lab’s text before it posts it.
Peeps accepts this branch although it doesn’t follow it, but only at the exact commit it pushed, and it files only the healed test:
- An upload that doesn’t report the healed test is refused with
422 test_not_reportedand files nothing. - An upload of another commit on that branch is refused with
422 branch_not_followed, and the message names the commit to send. - A later upload of the same commit, such as a run on a second camera model, is filed, and leaves the pull request as it is.
- If someone already closed or merged the pull request, or pushed to its branch, Peeps leaves it alone.
When no workflow runs the lab
When no workflow runs the lab on the fix’s branch, or Peeps can’t read your workflows, a draft would wait for a run nothing starts. So the pull request opens as Healed fixes arrive as says, and its description starts with Not yet run on the lab. It says which of the two it was.- Before it’s merged, an upload for its branch at its commit still counts: a failure closes the pull request, and a pass comments that the fix is verified on the lab and marks a draft ready for review.
- After it’s merged, the lab’s first upload of a followed branch at a commit that includes the merge comments on the merged pull request whether the fix held, for example “The lab’s results for
mainat9f8e7d6onCD62 · v2026.10.27.2, which includes this merged fix, reporttest_capturefailed: the merged fix did not hold.”
On GitLab, Peeps doesn’t read your CI configuration yet, so a fix whose hook printed
peeps:pending always opens as a draft that waits for the lab’s results.Example workflow
The hook runs before Peeps commits, so it can’t know the fix’s branch or commit. Your own CI can: run the lab on pushes topeeps/… branches. The fix’s commit names the test to run in two trailers:
Peeps-Lab-Test: the pytest node id of the test function, from the repository root, such astests/device/test_capture.py::test_capture. It selects every parametrization of the test.Peeps-Lab-Browser: for a pytest-playwright test, the browser to pass topytest --browser.
peeps/… branches, such as new tests Peeps writes, carry no trailer, so the workflow below skips them.
.github/workflows/lab-verify.yml
GITHUB_REF_NAME and GITHUB_SHA are the fix’s branch and commit, which is what Peeps waits for. If pytest’s rootdir isn’t the repository root, send it as rootDir, or Peeps can’t find the healed test and answers 422 test_not_reported. Add context to say which camera and firmware the lab used.
The same workflow file can also run the lab on your headline branch. Then the lab’s runs after a merge tell you whether each fix held.