Skip to content

Commit f413aa3

Browse files
authored
Merge pull request #44 from btotharye/feat/pre-commit
Feat/pre commit
2 parents d06d228 + 02b3466 commit f413aa3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+10263
-644
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
name: Greybeard Code Review
2+
3+
on:
4+
pull_request:
5+
branches: [main, develop]
6+
types: [labeled]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
checks: write
13+
14+
jobs:
15+
greybeard-review:
16+
name: Staff-Level Code Review
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 15
19+
if: |
20+
(github.event_name == 'pull_request' &&
21+
github.event.label.name == 'greybeard-review' &&
22+
github.event.pull_request.draft == false) ||
23+
github.event_name == 'workflow_dispatch'
24+
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
pack: ["staff-core", "oncall-future-you", "security-reviewer"]
29+
30+
steps:
31+
- name: Checkout PR branch
32+
uses: actions/checkout@v6
33+
with:
34+
fetch-depth: 0
35+
36+
- name: Checkout base branch for diff
37+
run: |
38+
git fetch origin ${{ github.base_ref }}
39+
40+
- name: Install uv
41+
uses: astral-sh/setup-uv@v7
42+
with:
43+
version: "latest"
44+
45+
- name: Install greybeard
46+
run: |
47+
uv tool install "greybeard[anthropic]"
48+
49+
- name: Configure Anthropic backend
50+
run: |
51+
greybeard config set llm.backend anthropic
52+
greybeard config set llm.model claude-haiku-4-5-20251001
53+
54+
- name: Validate API key
55+
env:
56+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
57+
run: |
58+
if [ -z "${ANTHROPIC_API_KEY}" ]; then
59+
echo "::error::ANTHROPIC_API_KEY secret is not set. Go to Settings → Secrets and variables → Actions → New repository secret."
60+
exit 1
61+
fi
62+
63+
- name: Generate git diff
64+
id: diff
65+
run: |
66+
git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr.diff
67+
ORIG_SIZE=$(wc -c < /tmp/pr.diff)
68+
echo "orig_size=$ORIG_SIZE" >> $GITHUB_OUTPUT
69+
# Hard skip if the raw diff exceeds 1MB — prevents runaway API cost from
70+
# accidentally large PRs or force-push storms.
71+
if [ "$ORIG_SIZE" -gt 1048576 ]; then
72+
echo "too_large=true" >> $GITHUB_OUTPUT
73+
echo "::warning::Diff is ${ORIG_SIZE} bytes (> 1MB). Greybeard review skipped to avoid excessive API cost. Reduce the diff or increase the limit in the workflow."
74+
else
75+
echo "too_large=false" >> $GITHUB_OUTPUT
76+
# Truncate to ~200KB (~50k tokens at avg 4 chars/token) — safe for all Anthropic models
77+
truncate -s 200k /tmp/pr.diff
78+
fi
79+
80+
- name: Analyze with Greybeard
81+
id: review
82+
if: steps.diff.outputs.too_large != 'true'
83+
continue-on-error: true
84+
env:
85+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
86+
run: |
87+
# Explicit 8-minute timeout — well within the 15-minute job limit.
88+
# If the LLM is slow or the diff is large, this exits with code 124
89+
# (timeout), which triggers the "Review unavailable" path below.
90+
REVIEW_OUTPUT=$(timeout 8m greybeard analyze \
91+
--pack "${{ matrix.pack }}" \
92+
--mode review \
93+
--format markdown < /tmp/pr.diff 2>&1)
94+
EXIT_CODE=$?
95+
echo "$REVIEW_OUTPUT" > /tmp/review_${{ matrix.pack }}.md
96+
exit $EXIT_CODE
97+
98+
- name: Check for blocking issues
99+
id: risk-check
100+
env:
101+
RISK_THRESHOLD: ${{ vars.GREYBEARD_RISK_THRESHOLD || 'high' }}
102+
run: |
103+
REVIEW_FILE="/tmp/review_${{ matrix.pack }}.md"
104+
if [ "$RISK_THRESHOLD" = "high" ]; then
105+
BLOCK_PATTERNS="production incident|data loss|security vulnerability|cascading failure"
106+
elif [ "$RISK_THRESHOLD" = "medium" ]; then
107+
BLOCK_PATTERNS="production incident|data loss|security vulnerability|cascading failure|operational overhead"
108+
else
109+
BLOCK_PATTERNS="risk|concern|careful|consider"
110+
fi
111+
BLOCKING_FOUND=0
112+
if grep -iE "$BLOCK_PATTERNS" "$REVIEW_FILE" > /dev/null 2>&1; then
113+
BLOCKING_FOUND=1
114+
fi
115+
echo "blocking=$BLOCKING_FOUND" >> $GITHUB_OUTPUT
116+
117+
- name: Post review as PR comment
118+
if: always()
119+
uses: actions/github-script@v7
120+
with:
121+
github-token: ${{ secrets.GITHUB_TOKEN }}
122+
script: |
123+
const fs = require('fs');
124+
const pack = '${{ matrix.pack }}';
125+
const reviewFile = `/tmp/review_${pack}.md`;
126+
const icons = {
127+
'staff-core': '🧙',
128+
'oncall-future-you': '📟',
129+
'security-reviewer': '🔒'
130+
};
131+
const icon = icons[pack] || '📋';
132+
// Unique hidden marker used for idempotent comment matching.
133+
// Avoids false matches if user text happens to contain the pack name.
134+
const marker = `<!-- greybeard-bot:${pack} -->`;
135+
const tooLarge = '${{ steps.diff.outputs.too_large }}' === 'true';
136+
if (tooLarge) {
137+
const sizeBody = marker + `\n## ${icon} Greybeard Review: ${pack}\n\n` +
138+
`⏭️ **Review skipped — diff too large** (> 1 MB).\n\n` +
139+
`Reduce the scope of this PR or raise the size limit in the workflow.\n\n` +
140+
`---\n_[Greybeard](https://github.com/btotharye/greybeard) · \`${{ github.sha }}\`_`;
141+
const { data: allComments } = await github.rest.issues.listComments({
142+
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number
143+
});
144+
const existingSize = allComments.find(c => c.user.type === 'Bot' && c.body.includes(marker));
145+
if (existingSize) {
146+
await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingSize.id, body: sizeBody });
147+
} else {
148+
await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: sizeBody });
149+
}
150+
return;
151+
}
152+
const reviewFailed = '${{ steps.review.outcome }}' === 'failure' || '${{ steps.review.outcome }}' === 'skipped';
153+
if (reviewFailed || !fs.existsSync(reviewFile)) {
154+
const errBody = marker + `\n## ${icon} Greybeard Review: ${pack}\n\n` +
155+
`⚠️ **Review unavailable** — greybeard could not complete this review.\n\n` +
156+
`Check the [Actions log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.\n\n` +
157+
`---\n_[Greybeard](https://github.com/btotharye/greybeard) · \`${{ github.sha }}\`_`;
158+
const { data: allComments } = await github.rest.issues.listComments({
159+
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number
160+
});
161+
const existing = allComments.find(c => c.user.type === 'Bot' && c.body.includes(marker));
162+
if (existing) {
163+
await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: errBody });
164+
} else {
165+
await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: errBody });
166+
}
167+
return;
168+
}
169+
const reviewContent = fs.readFileSync(reviewFile, 'utf8');
170+
const blocking = '${{ steps.risk-check.outputs.blocking }}' === '1';
171+
let truncated = reviewContent;
172+
if (truncated.length > 60000) {
173+
truncated = truncated.substring(0, 60000) + '\n\n... _(truncated)_';
174+
}
175+
const blockingBadge = blocking ? '⚠️ **BLOCKING ISSUES DETECTED**\n\n' : '';
176+
const comment = marker + `\n${blockingBadge}## ${icon} Greybeard Review: ${pack}\n\n${truncated}\n\n---\n_[Greybeard](https://github.com/btotharye/greybeard) · \`${{ github.sha }}\`_`;
177+
const { data: allComments } = await github.rest.issues.listComments({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
issue_number: context.issue.number
181+
});
182+
const existingComment = allComments.find(c =>
183+
c.user.type === 'Bot' &&
184+
c.body.includes(marker)
185+
);
186+
if (existingComment) {
187+
await github.rest.issues.updateComment({
188+
owner: context.repo.owner,
189+
repo: context.repo.repo,
190+
comment_id: existingComment.id,
191+
body: comment
192+
});
193+
} else {
194+
await github.rest.issues.createComment({
195+
owner: context.repo.owner,
196+
repo: context.repo.repo,
197+
issue_number: context.issue.number,
198+
body: comment
199+
});
200+
}
201+
202+
- name: Set PR status check
203+
if: always()
204+
uses: actions/github-script@v7
205+
with:
206+
github-token: ${{ secrets.GITHUB_TOKEN }}
207+
script: |
208+
const reviewFailed = '${{ steps.review.outcome }}' === 'failure';
209+
const blocking = '${{ steps.risk-check.outputs.blocking }}' === '1';
210+
const pack = '${{ matrix.pack }}';
211+
const conclusion = reviewFailed ? 'neutral' : (blocking ? 'failure' : 'success');
212+
await github.rest.checks.create({
213+
owner: context.repo.owner,
214+
repo: context.repo.repo,
215+
name: `Greybeard: ${pack}`,
216+
head_sha: context.sha,
217+
status: 'completed',
218+
conclusion: conclusion,
219+
output: {
220+
title: reviewFailed ? `Review Unavailable (${pack})` : `Staff Review (${pack})`,
221+
summary: reviewFailed
222+
? 'Greybeard could not complete the review — check the Actions log for details'
223+
: (blocking ? 'Blocking issues detected' : 'No blocking issues'),
224+
text: reviewFailed
225+
? 'The greybeard analysis step failed. This may be a transient API error or misconfigured pack. The PR is not blocked.'
226+
: `Greybeard analysis complete for ${pack} perspective.`
227+
}
228+
});

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ venv/
1313
htmlcov/
1414
.coverage
1515
coverage.xml
16+
# coverage annotate artifacts (*.py,cover files) — never commit these
17+
*.py,cover
18+
*.cover
1619
site/
1720
review.md

.pre-commit-hooks.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# greybeard pre-commit hooks
2+
#
3+
# IMPORTANT: Always pin to a specific release tag in your .pre-commit-config.yaml.
4+
# Using `rev: main` risks breaking all developers' commits if a bad commit lands on main.
5+
#
6+
# Recommended configuration:
7+
#
8+
# repos:
9+
# - repo: https://github.com/btotharye/greybeard
10+
# rev: v0.7.0 # pin to a release tag — never use 'main' in production teams
11+
# hooks:
12+
# - id: greybeard-diff
13+
# stages: [commit]
14+
#
15+
# Kill switch: if greybeard is blocking commits unexpectedly, set `enabled: false`
16+
# in your .greybeard-precommit.yaml to disable all hooks without removing the config:
17+
#
18+
# enabled: false
19+
20+
- id: greybeard-diff
21+
name: Greybeard Code Review
22+
description: Run greybeard Staff-level review on staged changes
23+
entry: greybeard-precommit diff
24+
language: python
25+
stages: [commit]
26+
types: [python, javascript, typescript, yaml, json, markdown]
27+
always_run: true
28+
require_serial: true
29+
30+
- id: greybeard-check
31+
name: Greybeard Risk Check
32+
description: Verify staged changes against configurable risk gates
33+
entry: greybeard-precommit check
34+
language: python
35+
stages: [commit]
36+
always_run: true
37+
require_serial: true

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
12+
- **Default Anthropic model changed from `claude-3-5-sonnet-20241022` to `claude-haiku-4-5-20251001`**
13+
This is a deliberate cost/speed optimisation for the GitHub Actions workflow (Haiku is ~
14+
cheaper and ~2× faster for typical diffs). **Review quality will be lower** than with Sonnet.
15+
Users who want Sonnet-level reviews should override explicitly:
16+
`greybeard config set llm.model claude-sonnet-4-6`
17+
or set the model in the workflow: `greybeard config set llm.model claude-sonnet-4-6`.
18+
19+
### Added
20+
21+
- GitHub Actions workflow: explicit 8-minute LLM timeout to prevent silent 15-minute job hangs
22+
- GitHub Actions workflow: 1 MB diff size guard skips oversized PRs instead of running them
23+
- GitHub Actions workflow: API key validation step gives a clear error if the secret is missing
24+
- GitHub Actions workflow: unique `<!-- greybeard-bot:PACK -->` HTML comment marker for
25+
idempotent comment updates (prevents false matches on user-created comments)
26+
- Pre-commit: structured "What to do" guidance on blocked commits
27+
- Pre-commit: helpful YAML parse error when `.greybeard-precommit.yaml` is malformed
28+
29+
### Fixed
30+
31+
- `PDFReporter.__init__` default arg `pagesize=letter` caused `NameError` at import time
32+
when `reportlab` is not installed; changed to `pagesize=None` with deferred assignment
33+
834
## [0.3.4] - 2026-03-15
935

1036
## Fixed

0 commit comments

Comments
 (0)