Install

LGTM is two workflow files and two secrets. There is no server to run, no database to provision, and no checkout: the diff and the configuration are both read through the API.

The two workflows

The split is a security property, not an accident.

.github/workflows/lgtm-collect.yml is deliberately powerless. It listens for pull_request_review with permissions: {}, writes the event to an artifact, and exits. On a pull request from a fork that event carries no secrets and a read-only token, so nothing else could happen there anyway.

name: LGTM (collect)
on:
  pull_request_review:
    types: [submitted]
 
permissions: {}
 
jobs:
  collect:
    if: github.event.review.state == 'approved'
    runs-on: ubuntu-latest
    steps:
      - env:
          EVENT: ${{ toJSON(github.event) }}
        run: |
          mkdir -p relay
          jq -n --arg name pull_request_review --argjson payload "$EVENT" \
            '{event_name: $name, payload: $payload}' > relay/event.json
      - uses: actions/upload-artifact@v4
        with:
          name: lgtm-event
          path: relay/event.json
          retention-days: 1

.github/workflows/lgtm.yml is the privileged half. It picks the relayed event up through workflow_run, and also listens to issue_comment directly, since that event does carry secrets and a write token even on fork pull requests.

name: LGTM
on:
  workflow_run:
    workflows: [LGTM (collect)]
    types: [completed]
  issue_comment:
    types: [created, edited]
 
permissions:
  contents: read        # read the diff
  checks: write         # post the review-confirmed check
  issues: write         # the quiz comment, replies, reactions
  pull-requests: write  # read the diff, react and reply
  actions: read         # download the relayed event artifact
 
jobs:
  lgtm:
    if: >
      github.event_name == 'workflow_run' ||
      (github.event.issue.pull_request != null &&
       (contains(github.event.comment.body, 'lgtm:v1') ||
        contains(github.event.comment.body, '/lgtm waive') ||
        contains(github.event.comment.body, '@lgtm')))
    runs-on: ubuntu-latest
    steps:
      - name: Fetch the relayed event
        if: github.event_name == 'workflow_run'
        uses: actions/download-artifact@v4
        with:
          name: lgtm-event
          path: relay
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
 
      - uses: sparepartslabs/spareparts-lgtm@v1
        with:
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          seal-key: ${{ secrets.LGTM_SEAL_KEY }}
          relayed-event: ${{ github.event_name == 'workflow_run' && 'relay/event.json' || '' }}

Declaring that permissions block sets every scope not listed to none. Each line is load-bearing; none of them is a default.

The privileged half holds the model key and a write token while handling data a fork author controls, so it never checks out or executes anything from the fork. The only inputs are the event payload and the diff, both read as data. If you add steps here, do not add a checkout of a fork ref.

Every input the action takes is declared in action.yml, including openai-api-key, gemini-api-key, github-token and bot-login.

The two secrets

Secret Required What it does
A model key Yes, one of them Writes the questions, verifies them, produces the reading aids and answers @lgtm.
LGTM_SEAL_KEY Yes Signs the quiz seal, so a tampered quiz is reissued rather than graded.

The model key is ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY, passed as the matching input. The provider field in .github/lgtm.yml picks between them and defaults to Anthropic.

No vendor here is a second-class choice. All three can search the web, which is what the reading aids and @lgtm answers need, so a repository running provider: openai gets everything a repository running Anthropic gets. A provider named without its key produces a neutral check, never a block.

Generate the seal key with:

openssl rand -base64 32

Keep it stable across runs. It has no fallback, and an unset seal key is a loud startup failure rather than a quiet one: a repository that cannot verify its own quizzes should say so rather than reissue a fresh quiz under whoever is answering.

What you will see

On approval, LGTM posts a comment carrying the quiz and a check run named LGTM — review confirmed. That name is a literal, em dash included, and branch protection matches it exactly. Answer by ticking a box per question. Wrong answers get a revisit note and the comment re-renders with your ticks intact. Right answers collapse the comment to one line and turn the check green.

By default that check never holds a merge. To change that, see enforcing.