> ## Documentation Index
> Fetch the complete documentation index at: https://docs.veecle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Check pull requests on a virtual board

> Run firmware on a virtual board from a pull request, and fail the job when the expected UART output is missing.

You want a pull request checked on a virtual board, and the job failed when the captured UART is missing what you expect.

## What you'll do

**1. Create a personal access token**

On [API keys](/platform/api-keys), create a personal access token and give it a name. Chiplab shows the token once. Copy it into a CI secret. The job sends it as a bearer token, `CHIPLAB_API_KEY`.

```json theme={null}
{
  "mcpServers": {
    "chiplab": {
      "type": "http",
      "url": "https://chiplab.veecle.ai/mcp",
      "headers": {
        "Authorization": "Bearer ${CHIPLAB_API_KEY}"
      }
    }
  }
}
```

`CHIPLAB_API_KEY` stays in the CI environment. The config file holds the `${CHIPLAB_API_KEY}` placeholder, so the token is expanded when the job connects.

**2. Start from the GitHub Actions template**

The examples repository ships [`.github/workflows/claude-chiplab-smoke.yml`](https://github.com/veecle/chiplab/blob/main/.github/workflows/claude-chiplab-smoke.yml). The template runs Claude Code in the job, which then uses Chiplab. It runs when someone comments `/smoke-test` on a pull request. It builds `examples/bare-metal/stm32f4-discovery` and checks the captured UART. It needs two secrets: `CHIPLAB_API_KEY` (a Chiplab personal access token, starts with `veecle_pat_`) and `ANTHROPIC_API_KEY` (for Claude Code). Treat it as a template: the trigger, the agent, and the example path are yours to change. Changing the `on:` trigger by itself is not enough. The job checks that the comment contains `/smoke-test`, and it checks out the pull request number from that comment.

**3. Run on every pull request**

To run on every `pull_request`, change the trigger, the job-level comment check, and the checkout together. A `pull_request` event has no comment body, and `github.event.issue.number` is empty.

```diff theme={null}
 on:
-  issue_comment:
-    types: [created]
+  pull_request:
```

Delete the job-level `if`. It looks for `/smoke-test` in the comment. Deleting it also drops the check that the commenter is `OWNER`, `MEMBER`, or `COLLABORATOR`.

```diff theme={null}
-    if: |
-      github.event.issue.pull_request
-      && contains(github.event.comment.body, '/smoke-test')
-      && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
```

Check out the pull request head:

```diff theme={null}
-          ref: refs/pull/${{ github.event.issue.number }}/head
+          ref: ${{ github.event.pull_request.head.sha }}
```

The fork-guard step calls `gh pr view` with that same number. Point it at the pull request:

```diff theme={null}
-          HEAD_REPO=$(gh pr view ${{ github.event.issue.number }} \
+          HEAD_REPO=$(gh pr view ${{ github.event.pull_request.number }} \
```

**4. Run on the virtual board**

The job calls `ask` for the current boards, tools, and gotchas, produces an ELF, uploads it, and runs it on the target board with the UART the firmware writes. It then waits and downloads `uart`.

**5. Fail when the expected output is missing**

Fail the pipeline unless `uart` contains what you expect. The public examples expect `Hello world!`. The check must look at the `uart` output, not the run status, because a crashing firmware still returns `succeeded`.

**6. Gate on each test**

To get a pass or fail per test, swap that UART check for `run_test_suite`. In your copy of the template, the prompt tells the agent to fail when the captured UART does not contain `Hello world!`. Change it so the agent uploads a suite, calls `run_test_suite`, and fails unless every test passed. `result.summary` has `planned`, `passed`, and `failed`. A null summary means the suite did not compile, or it threw before any test ran.

```diff theme={null}
-            Build the firmware in examples/bare-metal/stm32f4-discovery and test it on Chiplab, following the workflow in AGENTS.md. Diagnose and report any issues.
-
-            If the firmware does not build, or the captured UART output does not contain "Hello world!", exit with a non-zero status so the workflow fails.
+            Build the firmware in examples/bare-metal/stm32f4-discovery, upload it with a suite, and call run_test_suite, following the workflow in AGENTS.md. Diagnose and report any issues.
+
+            If the firmware does not build, result.summary is null, or result.summary.failed is not 0, exit with a non-zero status so the workflow fails.
```

## Ask your agent

```text theme={null}
Run this firmware on the STM32F4 Discovery, capture usart2, and fail the job if Hello world! is missing.
```

## What you get back

The job downloads `uart` and fails the pipeline unless that output contains what you expect. The public examples expect `Hello world!`.

## Next

> **[test](/tools/test)**
>
> Get a pass or fail for each test.

> **[run](/tools/run)**
>
> Execute an ELF on a virtual board and read the UART.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.