-
-
Notifications
You must be signed in to change notification settings - Fork 108
184 lines (171 loc) · 7.99 KB
/
Copy pathbot-propose-fix.yml
File metadata and controls
184 lines (171 loc) · 7.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
name: Wheels Bot — Propose Fix
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
issue-number:
description: 'Issue number to propose a fix for'
required: true
type: string
permissions:
contents: read
concurrency:
group: wheels-bot-propose-fix-${{ github.event.issue.number || inputs.issue-number }}
cancel-in-progress: false
jobs:
propose-fix:
name: Propose fix
runs-on: ubuntu-latest
timeout-minutes: 60
# Phase 4 (auto-fire): runs from wheels-bot[bot] comments containing a
# high- OR medium-confidence triage marker (bug path) or research marker
# (framework-design path). Low-confidence stays manual.
# Medium fires because propose-fix's own step-4 safety net (sensitive
# areas: security / migrations / deploy / DI / cross-engine) aborts
# before any PR is opened — the Reviewer catches the rest.
# workflow_dispatch is preserved for manual reruns.
# The bot-identity check is load-bearing: prevents humans from
# triggering propose-fix by quoting a marker in a comment reply.
if: |
vars.WHEELS_BOT_ENABLED == 'true'
&& (
github.event_name == 'workflow_dispatch'
|| (github.event_name == 'issue_comment'
&& github.event.comment.user.login == 'wheels-bot[bot]'
&& (contains(github.event.comment.body, 'wheels-bot:triage-confidence:high')
|| contains(github.event.comment.body, 'wheels-bot:triage-confidence:medium')
|| contains(github.event.comment.body, 'wheels-bot:research-confidence:high')
|| contains(github.event.comment.body, 'wheels-bot:research-confidence:medium')))
)
env:
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue-number }}
WHEELS_CI: "true"
steps:
- name: Generate App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.WHEELS_BOT_APP_ID }}
private-key: ${{ secrets.WHEELS_BOT_PRIVATE_KEY }}
- name: Checkout develop
uses: actions/checkout@v6
with:
ref: develop
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Skip check
id: gate
uses: ./.github/actions/wheels-bot-skip-check
with:
target-type: issue
target-number: ${{ env.ISSUE_NUMBER }}
marker-pattern: 'wheels-bot:fix:${{ env.ISSUE_NUMBER }}|wheels-bot:fix-held:${{ env.ISSUE_NUMBER }}'
github-token: ${{ steps.app-token.outputs.token }}
# Campaign guard: session-driven campaign PRs race bot drafts (4 bot
# drafts were superseded by human/session PRs on 2026-06-10 alone); the
# human/session branch wins. Skip the (expensive) model invocation when
# either signal shows a human is already on the issue:
# (a) a `peter/issue-<N>-*` campaign branch exists on origin, or
# (b) an open non-bot PR references "#<N>" in its body.
# Logs only — no issue comment. Manual workflow_dispatch bypasses the
# guard entirely: a maintainer dispatching by hand is explicit intent,
# including over a lingering stale campaign branch.
- name: Campaign guard
if: steps.gate.outputs.skip == 'false'
id: campaign
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
echo "::notice::Campaign guard bypassed: manual dispatch is explicit maintainer intent."
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::issue number must be numeric, got: $ISSUE_NUMBER"
exit 1
fi
# (a) session-driven campaign branch for this issue already on origin
if [ -n "$(git ls-remote --heads origin "peter/issue-${ISSUE_NUMBER}-*")" ]; then
echo "::notice::Campaign guard: branch peter/issue-${ISSUE_NUMBER}-* exists on origin — skipping bot draft (the human/session branch wins)."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# (b) an open non-bot PR whose body references this issue. The
# --search narrows server-side; jq re-verifies the body references
# "#<N>" with a trailing non-digit boundary (plain contains() would
# let issue 123 match a body referencing #1234) and that the author
# is not a bot. ISSUE_NUMBER is regex-safe: validated ^[0-9]+$ above.
open_nonbot=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open \
--search "${ISSUE_NUMBER} in:body" \
--json number,author,body \
--jq "[.[] | select(.author.is_bot | not) | select(.body | test(\"#${ISSUE_NUMBER}([^0-9]|\$)\"))] | length")
if [ "${open_nonbot}" != "0" ]; then
echo "::notice::Campaign guard: ${open_nonbot} open non-bot PR(s) reference #${ISSUE_NUMBER} — skipping bot draft (the human/session PR wins)."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Compute branch slug
if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false'
id: branch
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
title=$(gh issue view "$ISSUE_NUMBER" --json title --jq '.title')
slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
| cut -c1-50 \
| sed -E 's/-+$//')
[[ -z "$slug" ]] && slug="issue"
name="fix/bot-${ISSUE_NUMBER}-${slug}"
echo "name=$name" >> "$GITHUB_OUTPUT"
- name: Configure git + create branch
if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false'
run: |
git config user.name "wheels-bot[bot]"
git config user.email "wheels-bot[bot]@users.noreply.github.com"
git checkout -b "${{ steps.branch.outputs.name }}"
- name: Set up Wheels test environment
if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false'
uses: ./.github/actions/setup-wheels-test-env
with:
port: '60007'
install-playwright: 'false'
- name: Run Propose Fix
if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false'
uses: anthropics/claude-code-action@v1
with:
# This workflow fires from wheels-bot[bot] comments containing
# high- or medium-confidence triage or research markers. Allow
# that bot identity (and github-actions[bot] for consistency).
allowed_bots: 'wheels-bot[bot],github-actions[bot]'
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}
prompt: |
/propose-fix ${{ env.ISSUE_NUMBER }}
# Model policy: judging gate = fable, coding stages = opus, janitorial = sonnet.
claude_args: |
--model claude-opus-4-8
--max-turns 1000
--allowedTools "Bash(gh:*),Bash(git:*),Bash(bash tools/test-local.sh*),Bash(curl:*),Read,Edit,Write,Grep,Glob"
- name: Push branch
if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
if git diff --quiet origin/develop -- && git diff --cached --quiet; then
echo "No changes produced — nothing to push."
exit 0
fi
git push -u origin "${{ steps.branch.outputs.name }}"
- name: Stop Lucee server
if: always()
run: |
if [ -f /tmp/lucli-server.pid ]; then
kill $(cat /tmp/lucli-server.pid) 2>/dev/null || true
fi
lucli server stop 2>/dev/null || true