Skip to content

Commit 1b7a483

Browse files
committed
pr bot
1 parent d2428b1 commit 1b7a483

1 file changed

Lines changed: 287 additions & 0 deletions

File tree

.github/workflows/pr-bot.yaml

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
name: PR Bot
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
contents: write
9+
pull-requests: write
10+
issues: write
11+
actions: write
12+
attestations: write
13+
id-token: write
14+
packages: write
15+
16+
jobs:
17+
handle-command:
18+
# Only run on PR comments (not issue comments) and from authorized users
19+
if: |
20+
github.event.issue.pull_request &&
21+
startsWith(github.event.comment.body, '/') &&
22+
(
23+
github.event.comment.author_association == 'OWNER' ||
24+
github.event.comment.author_association == 'MEMBER' ||
25+
github.event.comment.author_association == 'COLLABORATOR'
26+
)
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Get PR info
30+
id: pr-info
31+
env:
32+
GH_TOKEN: ${{ github.token }}
33+
run: |
34+
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
35+
echo "head_sha=$(echo "$PR_DATA" | jq -r '.head.sha')" >> $GITHUB_OUTPUT
36+
echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_OUTPUT
37+
echo "base_ref=$(echo "$PR_DATA" | jq -r '.base.ref')" >> $GITHUB_OUTPUT
38+
39+
- name: Parse command
40+
id: parse
41+
run: |
42+
COMMENT="${{ github.event.comment.body }}"
43+
COMMAND=$(echo "$COMMENT" | head -1 | awk '{print $1}' | tr -d '/')
44+
ARGS=$(echo "$COMMENT" | head -1 | cut -d' ' -f2- -s)
45+
46+
echo "command=$COMMAND" >> $GITHUB_OUTPUT
47+
echo "args=$ARGS" >> $GITHUB_OUTPUT
48+
echo "Parsed command: $COMMAND, args: $ARGS"
49+
50+
- name: React to comment
51+
env:
52+
GH_TOKEN: ${{ github.token }}
53+
run: |
54+
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
55+
-f content='eyes' || true
56+
57+
- name: Handle /help
58+
if: steps.parse.outputs.command == 'help'
59+
env:
60+
GH_TOKEN: ${{ github.token }}
61+
run: |
62+
HELP_MSG=$(cat << 'EOF'
63+
## 🤖 PR Bot Commands
64+
65+
| Command | Description |
66+
|---------|-------------|
67+
| `/help` | Show this help message |
68+
| `/build` | Build all changed recipes in this PR |
69+
| `/build x86_64` | Build for specific host (x86_64, aarch64, riscv64) |
70+
| `/build <recipe>` | Build a specific recipe |
71+
| `/lint` | Run linter on changed recipes |
72+
| `/check` | Check upstream versions |
73+
| `/approve` | Approve and merge (maintainers only) |
74+
75+
<sub>Commands are only available to repository collaborators.</sub>
76+
EOF
77+
)
78+
79+
gh pr comment ${{ github.event.issue.number }} \
80+
--repo ${{ github.repository }} \
81+
--body "$HELP_MSG"
82+
83+
- name: Handle /build
84+
if: steps.parse.outputs.command == 'build'
85+
env:
86+
GH_TOKEN: ${{ github.token }}
87+
run: |
88+
ARGS="${{ steps.parse.outputs.args }}"
89+
PR_NUM="${{ github.event.issue.number }}"
90+
91+
# Determine host
92+
HOST="x86_64-Linux"
93+
case "$ARGS" in
94+
x86_64*|X86_64*) HOST="x86_64-Linux" ;;
95+
aarch64*|AARCH64*|arm64*) HOST="aarch64-Linux" ;;
96+
riscv64*|RISCV64*) HOST="riscv64-Linux" ;;
97+
all|ALL) HOST="ALL" ;;
98+
esac
99+
100+
# Post acknowledgment
101+
gh pr comment "$PR_NUM" \
102+
--repo ${{ github.repository }} \
103+
--body "🚀 **Build triggered!**
104+
105+
| | |
106+
|---|---|
107+
| **PR** | #${PR_NUM} |
108+
| **Host** | \`${HOST}\` |
109+
| **Triggered by** | @${{ github.event.comment.user.login }} |
110+
111+
Build will start shortly. I'll post results when complete.
112+
113+
[View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
114+
115+
# Trigger the build workflow
116+
gh workflow run pr-build-test.yaml \
117+
--repo ${{ github.repository }} \
118+
-f pr_number="$PR_NUM" \
119+
-f host="$HOST"
120+
121+
- name: Handle /lint
122+
if: steps.parse.outputs.command == 'lint'
123+
env:
124+
GH_TOKEN: ${{ github.token }}
125+
run: |
126+
PR_NUM="${{ github.event.issue.number }}"
127+
HEAD_SHA="${{ steps.pr-info.outputs.head_sha }}"
128+
BASE_REF="${{ steps.pr-info.outputs.base_ref }}"
129+
130+
gh pr comment "$PR_NUM" \
131+
--repo ${{ github.repository }} \
132+
--body "🔍 **Running linter...**"
133+
134+
# Checkout PR
135+
gh pr checkout "$PR_NUM" --repo ${{ github.repository }}
136+
137+
# Download linter
138+
curl -fsSL "https://github.com/pkgforge/sbuilder/releases/download/latest/sbuild-linter-x86_64-linux" \
139+
-o /tmp/sbuild-linter && chmod +x /tmp/sbuild-linter
140+
141+
# Get changed files
142+
CHANGED=$(git diff --name-only "origin/${BASE_REF}" HEAD -- 'binaries/**/*.yaml' 'packages/**/*.yaml' 2>/dev/null || true)
143+
144+
if [ -z "$CHANGED" ]; then
145+
gh pr comment "$PR_NUM" \
146+
--repo ${{ github.repository }} \
147+
--body "✅ **Lint complete** - No recipe files changed"
148+
exit 0
149+
fi
150+
151+
# Run linter on each file
152+
RESULTS="| Recipe | Status |
153+
|--------|--------|"
154+
HAS_ERRORS=false
155+
156+
for file in $CHANGED; do
157+
if [ -f "$file" ]; then
158+
if /tmp/sbuild-linter lint "$file" > /tmp/lint-output.txt 2>&1; then
159+
RESULTS="${RESULTS}
160+
| \`${file}\` | ✅ Valid |"
161+
else
162+
RESULTS="${RESULTS}
163+
| \`${file}\` | ❌ Invalid |"
164+
HAS_ERRORS=true
165+
fi
166+
fi
167+
done
168+
169+
if [ "$HAS_ERRORS" = true ]; then
170+
EMOJI="❌"
171+
STATUS="Lint Failed"
172+
else
173+
EMOJI="✅"
174+
STATUS="Lint Passed"
175+
fi
176+
177+
gh pr comment "$PR_NUM" \
178+
--repo ${{ github.repository }} \
179+
--body "## ${EMOJI} ${STATUS}
180+
181+
${RESULTS}
182+
183+
<sub>Triggered by @${{ github.event.comment.user.login }}</sub>"
184+
185+
- name: Handle /check
186+
if: steps.parse.outputs.command == 'check'
187+
env:
188+
GH_TOKEN: ${{ github.token }}
189+
run: |
190+
PR_NUM="${{ github.event.issue.number }}"
191+
192+
gh pr comment "$PR_NUM" \
193+
--repo ${{ github.repository }} \
194+
--body "🔍 **Checking upstream versions...**"
195+
196+
# Checkout PR
197+
gh pr checkout "$PR_NUM" --repo ${{ github.repository }}
198+
199+
# Download sbuild-meta
200+
curl -fsSL "https://github.com/pkgforge/sbuilder/releases/download/latest/sbuild-meta-x86_64-linux" \
201+
-o /tmp/sbuild-meta && chmod +x /tmp/sbuild-meta || {
202+
gh pr comment "$PR_NUM" \
203+
--repo ${{ github.repository }} \
204+
--body "⚠️ Failed to download sbuild-meta"
205+
exit 1
206+
}
207+
208+
# Run version check
209+
/tmp/sbuild-meta check-updates \
210+
--recipes ./binaries ./packages \
211+
--output /tmp/updates.json \
212+
--parallel 5 \
213+
--timeout 30 || true
214+
215+
if [ ! -f /tmp/updates.json ] || [ "$(jq 'length' /tmp/updates.json)" = "0" ]; then
216+
gh pr comment "$PR_NUM" \
217+
--repo ${{ github.repository }} \
218+
--body "✅ **Version check complete** - All packages are up to date!"
219+
exit 0
220+
fi
221+
222+
# Format results
223+
RESULTS="| Package | Current | Upstream |
224+
|---------|---------|----------|"
225+
226+
while read -r line; do
227+
pkg=$(echo "$line" | jq -r '.pkg')
228+
current=$(echo "$line" | jq -r '.current_version')
229+
upstream=$(echo "$line" | jq -r '.upstream_version')
230+
RESULTS="${RESULTS}
231+
| \`${pkg}\` | ${current} | **${upstream}** |"
232+
done < <(jq -c '.[]' /tmp/updates.json)
233+
234+
gh pr comment "$PR_NUM" \
235+
--repo ${{ github.repository }} \
236+
--body "## 📦 Version Check Results
237+
238+
${RESULTS}
239+
240+
<sub>Triggered by @${{ github.event.comment.user.login }}</sub>"
241+
242+
- name: Handle /approve
243+
if: steps.parse.outputs.command == 'approve'
244+
env:
245+
GH_TOKEN: ${{ github.token }}
246+
run: |
247+
PR_NUM="${{ github.event.issue.number }}"
248+
USER="${{ github.event.comment.user.login }}"
249+
ASSOC="${{ github.event.comment.author_association }}"
250+
251+
# Only owners/admins can approve
252+
if [[ "$ASSOC" != "OWNER" && "$ASSOC" != "MEMBER" ]]; then
253+
gh pr comment "$PR_NUM" \
254+
--repo ${{ github.repository }} \
255+
--body "⚠️ @${USER} Only repository owners/members can use \`/approve\`"
256+
exit 0
257+
fi
258+
259+
# Approve the PR
260+
gh pr review "$PR_NUM" \
261+
--repo ${{ github.repository }} \
262+
--approve \
263+
--body "✅ Approved via \`/approve\` command by @${USER}"
264+
265+
gh pr comment "$PR_NUM" \
266+
--repo ${{ github.repository }} \
267+
--body "✅ **PR Approved** by @${USER}
268+
269+
Ready to merge when checks pass."
270+
271+
- name: Handle unknown command
272+
if: |
273+
steps.parse.outputs.command != 'help' &&
274+
steps.parse.outputs.command != 'build' &&
275+
steps.parse.outputs.command != 'lint' &&
276+
steps.parse.outputs.command != 'check' &&
277+
steps.parse.outputs.command != 'approve'
278+
env:
279+
GH_TOKEN: ${{ github.token }}
280+
run: |
281+
COMMAND="${{ steps.parse.outputs.command }}"
282+
283+
gh pr comment ${{ github.event.issue.number }} \
284+
--repo ${{ github.repository }} \
285+
--body "❓ Unknown command: \`/${COMMAND}\`
286+
287+
Use \`/help\` to see available commands."

0 commit comments

Comments
 (0)