Skip to main content
Some pytest tests never run in your GitHub Actions or GitLab CI. A camera test, for example, runs on hardware that a lab service reserves. Your lab can send each run’s JUnit file to Peeps with an API key. Peeps files it as a CI run of your repository, at the commit and branch the lab tested, and analyzes the failures as it does for any CI run. The lab’s results can also settle a fix: when the lab can’t finish inside a heal, Peeps opens the fix and lets the lab’s next upload decide it. See Verify fixes on the lab.
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 one POST 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.
Replace 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. gitRef must be the headline branch or a branch listed under Tracked branches. Otherwise Peeps answers 422 branch_not_followed.
  • The commit. commitSha must be on that branch: its latest commit or an earlier one. Otherwise Peeps answers 422 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
A JUnit file always wins over the marker, and a non-zero exit ignores it. Print the marker last: Peeps reads only the end of the hook’s output. Once the hook says the lab will report later, Peeps doesn’t run the test again during that fix: it works from the failing run’s evidence and your code. It pushes the fix to its 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, paths and paths-ignore filters, matching paths against the files the fix changed. It doesn’t read a job’s own if:.
  • its file contains /ci-results, the path of the lab results endpoint.
The pull request then opens as a draft, whatever Healed fixes arrive as says. Its description names the workflow, and the branch and commit the lab must report, and shows the hook’s note as Lab run:. Its receipt reads Pending: the repository’s lab reports this test’s result after the heal. When the lab uploads results for that branch at that commit, Peeps settles the draft and comments on it:
  • A pass marks the pull request ready for review: “The lab’s results for peeps/fix-… at 1a2b3c4 report test_capture passed, so Peeps marked this pull request ready for review.”
  • A failure closes it without merging: “The lab’s results for peeps/fix-… at 1a2b3c4 report test_capture failed, 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.
Each parametrization of the test, such as a camera profile or a browser, counts: if any of them failed, the fix fails; otherwise, if any passed, it passes. The comment ends with the upload’s 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_reported and 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.
A draft whose results never arrive waits until you close it.

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 main at 9f8e7d6 on CD62 · v2026.10.27.2, which includes this merged fix, report test_capture failed: the merged fix did not hold.”
The same happens when someone merges a draft before the lab has run it. Only uploads to this endpoint count: a report file carries no commit, so it never settles a fix.
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 to peeps/… 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 as tests/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 to pytest --browser.
Other 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.