mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
In this commit, we separate the two concerns in the PR severity workflow: working out the severity, and applying it. The classify job inspects the PR and records its verdict (the severity level, whether to comment, and the comment body) to a few files. A second apply job reads those files and does the mechanical work of setting the label and posting the comment. Pulling the classification apart from the application keeps each job doing one thing and makes the flow easier to follow. The apply job takes the severity the classifier picked and checks it against the known set before touching a label, and posts the comment from a file via --body-file so the body is handled as plain data. We also turn off checkout credential persistence, since neither job needs a git credential on disk.
313 lines
14 KiB
YAML
313 lines
14 KiB
YAML
name: PR Severity Classification
|
|
|
|
on:
|
|
# Use pull_request_target so the workflow runs on fork PRs with the base
|
|
# repository's workflow definition. The classify job below reads PR metadata
|
|
# with a read-only token and never checks out or executes PR code; the write
|
|
# scope needed to apply the label lives in a separate, model-free job.
|
|
pull_request_target:
|
|
types: [opened, synchronize, labeled]
|
|
|
|
# Default the whole workflow to read-only. Each job opts into exactly the scope
|
|
# it needs: classify stays read-only (untrusted PR metadata reaches the model,
|
|
# so it must not hold a write token), apply takes pull-requests: write but runs
|
|
# no model.
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: pr-severity-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
classify:
|
|
name: Classify PR Severity
|
|
runs-on: ubuntu-latest
|
|
# Cap the model run: it fires on every pull_request_target synchronize with
|
|
# attacker-controllable input, so bound its runner-minute/token cost rather
|
|
# than inheriting GitHub's 6h default. Mirrors the dedupe workflow.
|
|
timeout-minutes: 10
|
|
# Read-only: the classifier only inspects PR metadata via the GitHub API.
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
# Skip if PR has skip-severity-check label.
|
|
# For labeled events, only run if 'reclassify' label was added.
|
|
if: |
|
|
!contains(github.event.pull_request.labels.*.name, 'skip-severity-check') &&
|
|
(github.event.action != 'labeled' || github.event.label.name == 'reclassify')
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 1
|
|
# Don't leave the job token in .git/config: nothing here needs a
|
|
# persisted git credential, and the classifier runs on untrusted
|
|
# fork-PR input.
|
|
persist-credentials: false
|
|
|
|
- name: Classify PR with Claude
|
|
# Pinned to a full commit SHA rather than the mutable @v1 tag: this step
|
|
# runs on pull_request_target with CLAUDE_CODE_OAUTH_TOKEN and
|
|
# GITHUB_TOKEN in-process and is reachable by any fork author, so a
|
|
# repointed tag would run attacker-controlled action code with those
|
|
# secrets present. Bump deliberately when updating.
|
|
uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1
|
|
env:
|
|
# gh (invoked by the Bash(gh pr view) tool) authenticates from
|
|
# GH_TOKEN; set it explicitly so classification doesn't depend on the
|
|
# action propagating its github_token input into the tool environment,
|
|
# matching the dedupe find-duplicates step.
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
with:
|
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Accept any PR author: this job holds only a read-only token, reads
|
|
# PR metadata via the API, and writes its verdict to a file. The
|
|
# privileged label/comment step runs separately without the model.
|
|
# "*" is safe ONLY while this job stays read-only. Before granting it
|
|
# a write token or a mutating tool (a write-capable gh subcommand, a
|
|
# Bash mutation), replace "*" with an explicit allowlist — otherwise
|
|
# any fork author's PR text would steer a privileged model.
|
|
allowed_non_write_users: "*"
|
|
|
|
# Pin the model so the severity decision (which gates the label the
|
|
# apply job applies) is reproducible from the workflow file and does
|
|
# not drift on an action-default change.
|
|
model: claude-sonnet-5
|
|
|
|
# The classifier only needs to read PR data and record its verdict to
|
|
# the workspace. It has no write-capable gh tools.
|
|
#
|
|
# Security note: because this runs on pull_request_target, the OAuth
|
|
# token above is present in-process while the model reads untrusted
|
|
# fork-PR text. The read-only github_token bounds what the model can
|
|
# WRITE via the API, but the in-process token's protection rests on
|
|
# this allowlist staying minimal — read-only `gh pr view` plus
|
|
# `Write`, with no network- or shell-mutating tool a prompt injection
|
|
# could use to exfiltrate it. Keep it that way: do not add `Bash`
|
|
# verbs beyond `gh pr view`, and never add a tool that can make
|
|
# outbound requests. (A stricter design would drop Bash entirely and
|
|
# pre-fetch PR metadata via the API in a separate step.)
|
|
claude_args: --allowedTools "Bash(gh pr view:*)" "Write"
|
|
|
|
prompt: |
|
|
You are a PR severity classifier for the lnd (Lightning Network Daemon) repository.
|
|
|
|
## Tool Constraints
|
|
|
|
You ONLY have access to:
|
|
- `gh pr view` - to read PR metadata
|
|
- `Write` - to record your verdict to files
|
|
|
|
You do NOT have access to `gh api`, `gh label`, `gh pr edit`,
|
|
`gh pr comment`, or any other command. Do not attempt to use them.
|
|
You do NOT apply labels or post comments yourself. A separate,
|
|
deterministic step reads the files you write and applies the label
|
|
and comment. Your job is only to classify and record the result.
|
|
|
|
## Your Task
|
|
|
|
Analyze PR #${{ github.event.pull_request.number }} and:
|
|
1. Determine its severity level based on the files changed
|
|
2. Record the severity, whether a comment should be posted, and the
|
|
comment body, to files (see "Output" below).
|
|
|
|
## Severity Levels
|
|
|
|
**CRITICAL** (severity-critical) - Requires expert review:
|
|
- lnwallet/* - Wallet operations, channel funding, signing, commitment transactions
|
|
- htlcswitch/* - HTLC forwarding, payment routing state machine
|
|
- contractcourt/* - On-chain dispute resolution, breach handling
|
|
- sweep/* - Output sweeping, fund recovery, fee bumping
|
|
- peer/*, brontide/* - Encrypted peer connections, Noise protocol
|
|
- keychain/* - Private key derivation and management
|
|
- input/* - Script signing, witness generation, MuSig2
|
|
- channeldb/* - Channel state persistence, database migrations
|
|
- funding/* - Channel funding workflow coordination
|
|
- lnwire/* - Lightning wire protocol messages
|
|
- server.go, rpcserver.go - Core server coordination
|
|
|
|
**HIGH** (severity-high) - Requires knowledgeable engineer:
|
|
- routing/* - Payment pathfinding algorithms
|
|
- invoices/* - Invoice management and settlement
|
|
- discovery/* - Gossip protocol
|
|
- graph/* - Network graph maintenance
|
|
- watchtower/* - Breach remediation
|
|
- feature/* - Feature bit management
|
|
- lnrpc/* - RPC/API definitions
|
|
- macaroons/*, walletunlocker/*, cert/* - Auth/security
|
|
- chainntnfs/*, chanacceptor/*, protofsm/*, sqldb/*
|
|
|
|
**MEDIUM** (severity-medium) - Focused review:
|
|
- cmd/* - CLI client commands (do NOT inherit severity from server-side packages with similar names)
|
|
- payments/*, autopilot/*, lncfg/*, chanfitness/*
|
|
- netann/*, kvdb/*, chanbackup/*, aezeed/*, tor/*
|
|
- zpay32/*, tlv/*, fn/*, record/*, amp/*
|
|
- *.proto files (API changes)
|
|
- Other Go files not categorized above
|
|
|
|
**LOW** (severity-low) - Best-effort review:
|
|
- docs/*, release-notes/*, *.md files
|
|
- scripts/*, tools/*, contrib/*, make/*, docker/*
|
|
- itest/*, lntest/*, *_test.go (test-only changes)
|
|
- .github/* (CI/CD configuration)
|
|
|
|
## Classification Rules
|
|
|
|
1. The HIGHEST severity file determines the PR severity
|
|
2. Classify files by their actual package path, NOT by filename keywords.
|
|
Files under cmd/* are CLI client code and should always be MEDIUM,
|
|
even if the filename contains a server-side package name (e.g.
|
|
cmd/commands/cmd_walletunlocker.go is MEDIUM, not HIGH).
|
|
3. Bump severity UP one level if:
|
|
- PR touches >20 files (excluding tests and auto-generated files)
|
|
- PR has >500 lines changed (excluding tests and auto-generated files)
|
|
- PR touches multiple distinct critical packages
|
|
4. Check for override labels first (severity-override-*). If present, respect the override.
|
|
5. Database migrations (channeldb/migration*, sqldb/*, wtdb/*) are always CRITICAL
|
|
|
|
## Files to Exclude from Line/File Counting
|
|
When calculating file count and lines changed for severity bumps, exclude:
|
|
- Test files: *_test.go, itest/*, lntest/*
|
|
- Auto-generated files: *.pb.go, *.pb.gw.go, *.pb.json.go, *.sql.go, *_generated.go
|
|
- Mock files: mock_*.go, *_mock.go
|
|
|
|
## Steps
|
|
|
|
1. Read the current labels AND comments to detect overrides and prior
|
|
bot activity:
|
|
```
|
|
gh pr view ${{ github.event.pull_request.number }} --json labels,comments
|
|
```
|
|
Note which `severity-*` label (if any) is currently applied. This
|
|
is the "previous severity". Look for the HTML marker
|
|
`<!-- pr-severity-bot -->` in comment bodies to tell whether the
|
|
bot has commented before.
|
|
|
|
2. If an override label exists (severity-override-*), use that level
|
|
and skip classification.
|
|
|
|
3. Get the list of changed files:
|
|
```
|
|
gh pr view ${{ github.event.pull_request.number }} --json files,additions,deletions
|
|
```
|
|
|
|
4. Classify each file and determine the new overall severity.
|
|
|
|
5. **Decide whether a comment should be posted.** Set should_comment
|
|
to "true" only if EITHER:
|
|
- The bot has NOT commented before (no existing comment with
|
|
`<!-- pr-severity-bot -->`), OR
|
|
- The newly determined severity is DIFFERENT from the previous
|
|
severity label.
|
|
|
|
Otherwise set should_comment to "false" (the label may still be
|
|
updated by the apply step, but no new comment is posted).
|
|
|
|
## Output
|
|
|
|
Record your verdict by writing these files in the current working
|
|
directory (the repository root). Do NOT apply labels or comment
|
|
yourself.
|
|
|
|
1. `severity.txt` - exactly one lowercase word, one of:
|
|
`critical`, `high`, `medium`, `low`. Nothing else.
|
|
|
|
2. `should_comment.txt` - exactly `true` or `false`.
|
|
|
|
3. `comment.md` - only if should_comment is `true`. The full comment
|
|
markdown, in this format:
|
|
|
|
If this is a severity CHANGE (previous label existed but differs),
|
|
prepend: `> ⚠️ Severity changed: **<OLD>** → **<NEW>** (files changed since last classification)`
|
|
|
|
```markdown
|
|
## <emoji> PR Severity: **<LEVEL>**
|
|
|
|
> <source> | <N> files | <M> lines changed
|
|
|
|
<details>
|
|
<summary>🔴 <strong>Critical</strong> (N files)</summary>
|
|
|
|
- `path/to/file1.go` - reason
|
|
- `path/to/file2.go` - reason
|
|
|
|
</details>
|
|
|
|
[repeat for other tiers if applicable]
|
|
|
|
### Analysis
|
|
|
|
<Your explanation of why this severity was chosen, any concerns, etc.>
|
|
|
|
---
|
|
<sub>To override, add a `severity-override-{critical,high,medium,low}` label.</sub>
|
|
<!-- pr-severity-bot -->
|
|
```
|
|
|
|
If should_comment is `false`, do not create `comment.md`.
|
|
|
|
Keep the comment concise and factual: it is posted verbatim under
|
|
the bot's identity, and the apply step defangs any `@`-mentions
|
|
and links, so do not rely on them.
|
|
|
|
## Emoji Mapping
|
|
- critical: 🔴
|
|
- high: 🟠
|
|
- medium: 🟡
|
|
- low: 🟢
|
|
|
|
- name: Upload classification result
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: pr-severity-result
|
|
path: |
|
|
severity.txt
|
|
should_comment.txt
|
|
comment.md
|
|
if-no-files-found: warn
|
|
retention-days: 1
|
|
|
|
apply:
|
|
name: Apply Severity Label
|
|
runs-on: ubuntu-latest
|
|
needs: classify
|
|
timeout-minutes: 5
|
|
# Write scope lives here, in a job that runs no model. The only inputs are
|
|
# the PR number from the trusted event payload and the classifier's files,
|
|
# which are strictly validated before use.
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# Needed for scripts/apply-pr-severity.sh; no persisted git credential
|
|
# is required here.
|
|
persist-credentials: false
|
|
|
|
- name: Download classification result
|
|
uses: actions/download-artifact@v4
|
|
# classify uploads with if-no-files-found: warn, so if the model writes
|
|
# no verdict at all (timeout, refusal) no artifact exists and
|
|
# download-artifact would otherwise hard-fail the job. Tolerate a
|
|
# missing artifact so the no-op guard in the next step is reachable.
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-severity-result
|
|
path: result
|
|
|
|
- name: Apply label and comment
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
REPO: ${{ github.repository }}
|
|
# Validate the severity, reconcile the label, and sanitize + post the
|
|
# model-authored comment. The logic lives in a checked-in script so the
|
|
# untrusted-comment sanitizer is unit-tested
|
|
# (scripts/apply-pr-severity_test.sh), mirroring how the dedupe workflow
|
|
# delegates to scripts/comment-on-duplicates.sh.
|
|
run: ./scripts/apply-pr-severity.sh result
|