Zum Inhalt

Pull-Request-Lint

Prüft Titel und Body eines Pull Requests gegen die Pull-Request-Workflow-Spec des Portfolios (spec/project/pull-request-workflow/ in nolte/claude-shared):

  • den Conventional-Commits-Titel mit einem Typ aus feat, fix, chore, docs, exp
  • die fünf Pflichtabschnitte im Body in fester Reihenfolge sowie nicht leere Abschnitte Summary, Changes und Testing
  • einen Abschnitt ## Class sweep bei jedem fix-Pull-Request
  • Originating source: und Dispatched specialist: in ## Risk / rollout notes, wenn ## Linked issues ein Issue mit dem Audit-Label referenziert
  • optional eine Zeile Refs spec/<topic>/<slug>/, wenn die Änderung Pfade außerhalb von spec/ berührt

Ein Pull Request eines freigegebenen Dependency-Bots durchläuft nur die Titelprüfung.


Verwendung

.github/workflows/pr-lint.yml
on:
  pull_request:
    types: [opened, edited, synchronize, ready_for_review]

permissions:
  contents: read

jobs:
  pr-lint:
    permissions:
      contents: read
      pull-requests: read
      issues: read
    uses: nolte/gh-plumbing/.github/workflows/reusable-pr-lint.yaml@<tag-or-commit-sha>
    with:
      spec-anchor: false

Der Aufrufer muss pull-requests: read und issues: read gewähren, denn ein aufgerufener Workflow kann die Rechte seines Aufrufers nicht erweitern.

Input Default Wirkung
python-version 3.14 Python-Version für den Checker
audit-label audit Label, das ein Audit-Tracking-Issue markiert; leer schaltet die Traceability-Regel ab
spec-anchor false Verlangt eine Zeile Refs spec/…, wenn sich Pfade außerhalb von spec/ ändern

Erforderlicher Status-Check

Verlange den Context pr-lint / PR Lint auf develop; der Name setzt sich aus der Job-ID des Aufrufers und dem Job-Namen dieses Workflows zusammen — siehe Settings.

Der Checker läuft am gepinnten Commit

Der Workflow checkt seinen eigenen Checker an dem Commit aus, den der Aufrufer gepinnt hat, über job.workflow_repository und job.workflow_sha. Das Repository des Aufrufers checkt er nie aus, sodass ein Pull Request die Regeln, nach denen er geprüft wird, nicht ändern kann.


Zentrale Konfiguration

.github/workflows/reusable-pr-lint.yaml
name: Reusable — PR Lint

# Lints a pull request's title and body against the portfolio's
# spec/project/pull-request-workflow/ in nolte/claude-shared, which SHOULDs this
# linter as a reusable workflow here so every repository inherits one
# implementation (claude-shared#577):
#
# - the Conventional-Commits title with a type in {feat, fix, chore, docs, exp}
# - the five required body sections, in order, and the non-empty rule for
#   Summary, Changes and Testing
# - the `## Class sweep` section on a `fix` pull request
# - the remediation traceability fields when `## Linked issues` references an
#   issue carrying `audit-label` (claude-shared#616)
# - optionally, a `Refs spec/<topic>/<slug>/` anchor when implementation paths
#   change (`spec-anchor`)
#
# The checker comes from this repository at the exact commit the caller pinned
# (`job.workflow_repository` / `job.workflow_sha`), never from the caller, so a
# pull request can't change the rules it is judged by. The caller's repository
# isn't checked out at all. Title, body, author and every input reach the
# checker through the environment, never through `${{ }}` in a shell command.

on:
  workflow_call:
    inputs:
      python-version:
        description: Python version for the checker.
        required: false
        type: string
        default: "3.14"
      audit-label:
        description: |
          Label that marks an audit tracking issue. A pull request whose
          `## Linked issues` references such an issue must carry
          `Originating source:` and `Dispatched specialist:` in its Risk /
          rollout notes. Empty disables the rule.
        required: false
        type: string
        default: audit
      spec-anchor:
        description: |
          Require a `Refs spec/<topic>/<slug>/` line in `## Linked issues` when
          the pull request changes paths outside `spec/`. For repositories that
          keep a spec corpus.
        required: false
        type: boolean
        default: false

permissions:
  contents: read

jobs:
  pr-lint:
    name: PR Lint
    runs-on: ubuntu-latest
    # pull-requests: read lists the changed files for the spec-anchor rule;
    # issues: read reads the labels of linked issues for the traceability rule.
    permissions:
      contents: read
      pull-requests: read
      issues: read
    steps:
      - name: Check out the checker at the pinned commit
        uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          repository: ${{ job.workflow_repository }}
          ref: ${{ job.workflow_sha }}
          path: .pr-lint
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
        with:
          python-version: ${{ inputs.python-version }}

      - name: List the pull request's changed files
        if: ${{ inputs.spec-anchor }}
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename' > "${RUNNER_TEMP}/changed-files.txt"

      # The checker parses `## Linked issues` itself and writes issue numbers of
      # at most nine digits; the next step re-validates every line before it
      # reaches the API.
      - name: List the linked issue numbers
        if: ${{ inputs.audit-label != '' }}
        env:
          PR_BODY: ${{ github.event.pull_request.body }}
        run: python3 .pr-lint/scripts/pr-lint/check_pr_body.py --print-linked-issues > "${RUNNER_TEMP}/linked-issues.txt"

      # A reference GitHub answers with 404 or 410 (a discussion, a deleted
      # issue) carries no labels and is skipped; any other failure fails the
      # check, since the traceability rule can't be judged without the labels.
      - name: Read the labels of the linked issues
        if: ${{ inputs.audit-label != '' }}
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          count=$(wc -l < "${RUNNER_TEMP}/linked-issues.txt")
          if [ "${count}" -gt 50 ]; then
            echo "::error::## Linked issues references ${count} issues; the check reads at most 50"
            exit 1
          fi
          : > "${RUNNER_TEMP}/linked-issue-labels.txt"
          unreadable=""
          while read -r n; do
            if ! [[ "${n}" =~ ^[0-9]{1,9}$ ]]; then
              echo "::error::unexpected linked-issue line from the checker"
              exit 1
            fi
            if ! labels=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${n}" --jq '.labels[].name' 2> "${RUNNER_TEMP}/gh-error.txt"); then
              if grep -Eq 'HTTP (404|410)' "${RUNNER_TEMP}/gh-error.txt"; then
                echo "::notice::linked reference #${n} isn't a readable issue (404/410); skipped"
                continue
              fi
              unreadable="${unreadable} #${n}"
              continue
            fi
            printf '%s\n' "${labels}" | sed '/^$/d' | sed "s/^/${n} /" >> "${RUNNER_TEMP}/linked-issue-labels.txt"
          done < "${RUNNER_TEMP}/linked-issues.txt"
          if [ -n "${unreadable}" ]; then
            echo "::error::could not read the labels of linked issue(s)${unreadable}"
            exit 1
          fi

      - name: Lint the pull-request title and body
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BODY: ${{ github.event.pull_request.body }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
          PR_HEAD_REF: ${{ github.head_ref }}
          SPEC_ANCHOR: ${{ inputs.spec-anchor }}
          AUDIT_LABEL: ${{ inputs.audit-label }}
          CHANGED_FILES_FILE: ${{ inputs.spec-anchor && format('{0}/changed-files.txt', runner.temp) || '' }}
          LINKED_ISSUE_LABELS_FILE: ${{ inputs.audit-label != '' && format('{0}/linked-issue-labels.txt', runner.temp) || '' }}
        run: python3 .pr-lint/scripts/pr-lint/check_pr_body.py