docs Home GitHub

perch in CI

What CI can gate on, what it cannot, and a GitHub Actions job for pull requests.

perch scan exits 1 when a rule you wrote in perch.yaml is broken, and 0 otherwise.

That split is deliberate. A rule is a claim you made about your own code, so failing a build on it is you holding yourself to it. A finding perch turned up on its own is a probability, and exiting on one would make every run a coin toss.

So CI gates on your rules, and reports everything else.

Only what the branch changed#

--since narrows the scan to what moved:

perch scan --since origin/main

A pull request reads the methods it touched and their neighbourhood, not the whole repository. That is what keeps the job to a sensible size and cost.

GitHub Actions#

name: perch
on: pull_request

jobs:
  perch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # --since needs the base branch

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm install -g @lakeday/perch

      - name: scan what this branch changed
        env:
          TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
        run: perch scan --since origin/${{ github.base_ref }}

      - name: the worst of what it found
        if: always()
        run: perch issues --all --min 80

fetch-depth: 0 matters. Without the base branch in the checkout, --since has nothing to compare against.

Reporting without gating#

To collect findings without failing anything, read the JSON and decide yourself:

perch scan --since origin/main --json > perch.json || true
jq '[.[] | select(.issues[]? | .type == "security" and .probability > 0.9)]' perch.json

Sharing what the team set aside#

perch close writes to .perch/closed.jsonl, which is separate from the answers precisely so it can be committed. Commit it and the team's dismissals travel with the repository, so CI does not re-report what somebody already looked at.

Add the rest of .perch to .gitignore:

.perch/*
!.perch/closed.jsonl

Cost#

One HTTP request per method read. --since decides how many methods that is, --parallel decides how fast they go, and neither changes the total. Output tokens are not billed, so the width of the question set is not what you are paying for.

Edit this page on GitHub