wralph 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +275 -0
- data/bin/wralph +60 -0
- data/lib/wralph/adapters/agents/claude_code.rb +19 -0
- data/lib/wralph/adapters/agents.rb +9 -0
- data/lib/wralph/adapters/cis/circle_ci.rb +199 -0
- data/lib/wralph/adapters/cis.rb +9 -0
- data/lib/wralph/adapters/objective_repositories/github_issues.rb +37 -0
- data/lib/wralph/adapters/objective_repositories.rb +9 -0
- data/lib/wralph/config.rb +89 -0
- data/lib/wralph/fixtures/config.yaml +18 -0
- data/lib/wralph/fixtures/secrets.yaml +5 -0
- data/lib/wralph/interfaces/agent.rb +13 -0
- data/lib/wralph/interfaces/ci.rb +108 -0
- data/lib/wralph/interfaces/objective_repository.rb +81 -0
- data/lib/wralph/interfaces/print.rb +32 -0
- data/lib/wralph/interfaces/repo.rb +75 -0
- data/lib/wralph/interfaces/shell.rb +60 -0
- data/lib/wralph/run/execute_plan.rb +185 -0
- data/lib/wralph/run/feedback.rb +116 -0
- data/lib/wralph/run/init.rb +98 -0
- data/lib/wralph/run/iterate_ci.rb +120 -0
- data/lib/wralph/run/plan.rb +107 -0
- data/lib/wralph/run/remove.rb +43 -0
- data/lib/wralph/version.rb +5 -0
- data/lib/wralph.rb +19 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b57c8d8dca1cee5599eb0948a7eb9cbab5032bd33c9eb6aa879fbf7ec3b590b3
|
|
4
|
+
data.tar.gz: b04697444291bd7749c80a26a3f2cc29710f8fa9ef2d76a89c9334450ece3c52
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 727b377643d3159fa5832d0c67c08b207e11bd806648e652913f80b4b38412c929c6c028bfd92c55037ab59f4c56c919421ff7cced18733512ee29d568dab083
|
|
7
|
+
data.tar.gz: 6e12faa9ada5ac909b82e29f5850820cbd5c98407789632d6392a1fbe5eb1fd3cbafe5c6803fbd63f91851d0166af80a5b86f657b319df41679413a690551c4c
|
data/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# WRALPH - Workflow Ralph
|
|
2
|
+
|
|
3
|
+
**WRALPH** (short for **Workflow Ralph**) is an implementation of the "Ralph Wiggum" technique of using AI agents to iteratively develop code until complete.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This toolset provides a streamlined workflow for fixing bugs, implementing features, or making code changes by combining AI-powered code generation with human oversight. The process flows from issue repository pull → plan generation → execution → CI iteration → human review.
|
|
8
|
+
|
|
9
|
+
## What Makes WRALPH Distinct
|
|
10
|
+
|
|
11
|
+
What sets WRALPH apart from other autonomous development tools is its explicit use of **non-AI components** to facilitate the workflow and allow multiple instances running (even on the same repository) without interference:
|
|
12
|
+
|
|
13
|
+
- **Issue Repository**: Plans are generated from structured issue repositories (default: GitHub issues), providing clear requirements and context
|
|
14
|
+
- **Git Worktrees**: Each issue gets its own isolated worktree, ensuring clean separation and preventing conflicts
|
|
15
|
+
- **Remote CI Evaluation**: Results are evaluated using remote CI (default: CircleCI), which allows multiple instances of WRALPH to run in parallel without interfering with each other
|
|
16
|
+
|
|
17
|
+
The remote CI feature is particularly powerful—it enables you to run multiple instances of WRALPH simultaneously, each working on different issues, without the processes stepping on each other. This parallel execution capability makes WRALPH highly scalable for teams working on multiple issues concurrently.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TD
|
|
23
|
+
Start[Create GitHub Issue] --> Init[wralph init]
|
|
24
|
+
Init --> Plan[wralph plan ISSUE_NUMBER]
|
|
25
|
+
Plan --> PlanGen[AI Generates Plan]
|
|
26
|
+
PlanGen --> Review{Human Reviews Plan}
|
|
27
|
+
Review -->|Needs Changes| PlanGen
|
|
28
|
+
Review -->|Approved| Execute[Execute Plan]
|
|
29
|
+
Execute --> Code[AI Makes Code Changes]
|
|
30
|
+
Code --> PR[Create Pull Request]
|
|
31
|
+
PR --> CI[Monitor CI Build]
|
|
32
|
+
CI --> CheckCI{CI Passes?}
|
|
33
|
+
CheckCI -->|No| Fix[AI Analyzes Failures]
|
|
34
|
+
Fix --> Push[Push Fixes]
|
|
35
|
+
Push --> CI
|
|
36
|
+
CheckCI -->|Yes| HumanReview{Human Reviews PR}
|
|
37
|
+
HumanReview -->|Changes Requested| Feedback[wralph feedback ISSUE_NUMBER]
|
|
38
|
+
Feedback --> Changes[AI Makes Changes]
|
|
39
|
+
Changes --> CI
|
|
40
|
+
HumanReview -->|Approved| Cleanup[wralph remove ISSUE_NUMBER]
|
|
41
|
+
Cleanup --> Done[Complete]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Prerequisites
|
|
45
|
+
|
|
46
|
+
Before using these scripts, ensure you have the following tools installed:
|
|
47
|
+
|
|
48
|
+
- **[Ruby](https://www.ruby-lang.org/)** `>= 3.0` - Ruby interpreter (use rbenv, rvm, or Homebrew to install)
|
|
49
|
+
- **[GitHub CLI](https://cli.github.com/)** (`gh`) - Must be authenticated (`gh auth login`)
|
|
50
|
+
- **[Claude Code CLI](https://code.claude.com/)** (`claude`) - For AI-powered code generation
|
|
51
|
+
- **[jq](https://stedolan.github.io/jq/)** - JSON processor (install via `brew install jq` on macOS)
|
|
52
|
+
- **[curl](https://curl.se/)** - HTTP client (usually pre-installed)
|
|
53
|
+
- **[worktrunk](https://github.com/max-sixty/worktrunk)** (`wt`) - Git worktree management tool
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
After running `wralph init`, you'll have a `.wralph` directory with configuration files:
|
|
58
|
+
|
|
59
|
+
### Secrets (`secrets.yaml`)
|
|
60
|
+
|
|
61
|
+
Add your CI API token to `.wralph/secrets.yaml`:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
ci_api_token: your_token_here
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This file is automatically git-ignored for security.
|
|
68
|
+
|
|
69
|
+
### Adapter Configuration (`config.yaml`)
|
|
70
|
+
|
|
71
|
+
The `.wralph/config.yaml` file allows you to configure which adapters to use:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
# Objective repository adapter (where issues/objectives are stored)
|
|
75
|
+
objective_repository:
|
|
76
|
+
source: github_issues # or "custom" with class_name
|
|
77
|
+
|
|
78
|
+
# CI/CD adapter (for build monitoring)
|
|
79
|
+
ci:
|
|
80
|
+
source: circle_ci # or "custom" with class_name
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Custom Adapters**: You can create custom adapters by:
|
|
84
|
+
1. Setting `source: custom` in the config
|
|
85
|
+
2. Specifying a `class_name` (e.g., `MyCustomAdapter`)
|
|
86
|
+
3. Creating a file `.wralph/my_custom_adapter.rb` with your class implementation
|
|
87
|
+
|
|
88
|
+
See the config file comments for more details on creating custom adapters.
|
|
89
|
+
|
|
90
|
+
## Installation
|
|
91
|
+
|
|
92
|
+
If you're installing WRALPH (once it's distributed via Homebrew):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
brew install wralph # When available
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For development:
|
|
99
|
+
|
|
100
|
+
1. Ensure you have Ruby 3.0 or higher installed
|
|
101
|
+
2. Clone this repository
|
|
102
|
+
3. Install dependencies: `bundle install`
|
|
103
|
+
|
|
104
|
+
## Usage
|
|
105
|
+
|
|
106
|
+
### 1. Initialize WRALPH
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
wralph init
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This creates the `.wralph` directory structure needed for plans and configuration.
|
|
113
|
+
|
|
114
|
+
### 2. Create and Plan an Issue
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
wralph plan 123
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This command:
|
|
121
|
+
- Fetches GitHub issue #123
|
|
122
|
+
- Uses Claude Code to generate a detailed plan saved to `.wralph/plans/plan_123.md`
|
|
123
|
+
- Prompts you to review and approve the plan
|
|
124
|
+
- Automatically proceeds to execution once approved
|
|
125
|
+
|
|
126
|
+
The plan includes:
|
|
127
|
+
- Analysis of the issue
|
|
128
|
+
- Approach to solving it
|
|
129
|
+
- Test cases to verify the solution
|
|
130
|
+
- Implementation steps
|
|
131
|
+
- Potential risks or considerations
|
|
132
|
+
- Questions for clarification (if needed)
|
|
133
|
+
|
|
134
|
+
After plan approval, `wralph plan` automatically:
|
|
135
|
+
- Creates a git worktree for branch `issue-123`
|
|
136
|
+
- Uses Claude Code to implement the plan
|
|
137
|
+
- Commits changes (including the plan file) with a descriptive message
|
|
138
|
+
- Pushes the branch and creates a pull request
|
|
139
|
+
- Monitors CI build status for the PR (default: CircleCI)
|
|
140
|
+
- If CI fails: extracts failure details, uses Claude Code to fix issues, pushes fixes, and iterates (up to 10 retries)
|
|
141
|
+
- If CI passes: exits successfully
|
|
142
|
+
|
|
143
|
+
**Note:** The AI is instructed not to run tests locally, but instead to push a PR and rely on CI.
|
|
144
|
+
|
|
145
|
+
Failure details are saved to `tmp/issue-{NUMBER}_failure_details_{ITERATION}.txt` for reference.
|
|
146
|
+
|
|
147
|
+
### 3. Handle PR Review Feedback
|
|
148
|
+
|
|
149
|
+
When reviewers request changes on a PR:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
wralph feedback 123
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This command:
|
|
156
|
+
- Switches to the worktree for branch `issue-123`
|
|
157
|
+
- Prompts you to enter feedback (multi-line input, press Enter 3 times to submit)
|
|
158
|
+
- Uses Claude Code to analyze the feedback and make changes
|
|
159
|
+
- Pushes the changes and monitors CI again (with automatic retries if CI fails)
|
|
160
|
+
|
|
161
|
+
### 4. Clean Up (Optional)
|
|
162
|
+
|
|
163
|
+
To delete a branch and its worktree after completion:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
wralph remove 123
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This removes:
|
|
170
|
+
- Local branch `issue-123`
|
|
171
|
+
- Remote branch `issue-123`
|
|
172
|
+
- Associated worktree
|
|
173
|
+
|
|
174
|
+
## Commands Overview
|
|
175
|
+
|
|
176
|
+
| Command | Purpose |
|
|
177
|
+
|---------|---------|
|
|
178
|
+
| `wralph init` | Initialize WRALPH in the current repository |
|
|
179
|
+
| `wralph plan <issue_number>` | Generate plan and execute it (creates PR, monitors CI) |
|
|
180
|
+
| `wralph feedback <issue_number>` | Handle PR review feedback and iterate |
|
|
181
|
+
| `wralph remove <issue_number>` | Clean up branches and worktrees |
|
|
182
|
+
|
|
183
|
+
## Directory Structure
|
|
184
|
+
|
|
185
|
+
The tool creates and manages the following:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
.
|
|
189
|
+
├── .wralph/ # WRALPH configuration directory (created by `wralph init`)
|
|
190
|
+
│ ├── config.yaml # Adapter configuration (objective_repository, ci)
|
|
191
|
+
│ ├── secrets.yaml # API tokens and secrets (git-ignored)
|
|
192
|
+
│ └── plans/ # Generated plans
|
|
193
|
+
│ └── plan_123.md # Example plan file
|
|
194
|
+
├── tmp/ # CI failure details (created automatically)
|
|
195
|
+
│ └── issue-123_failure_details_1_1.txt
|
|
196
|
+
└── [worktree directories] # Managed by worktrunk
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Key Features
|
|
200
|
+
|
|
201
|
+
- **Worktree Isolation**: Each issue gets its own git worktree via worktrunk, keeping your main working directory clean
|
|
202
|
+
- **Automatic CI Integration**: Monitors CI builds (configurable adapter), extracts failure details, and iteratively fixes issues
|
|
203
|
+
- **Configurable Adapters**: Support for custom objective repositories and CI adapters via configuration
|
|
204
|
+
- **Human Oversight**: Plans require approval before execution, and PRs can be reviewed before merging
|
|
205
|
+
- **Failure Recovery**: Automatically retries up to 10 times to fix CI failures
|
|
206
|
+
- **Branch Management**: Automatic branch creation, PR creation, and cleanup utilities
|
|
207
|
+
- **Plan Persistence**: All plans are saved for reference and context during iterations
|
|
208
|
+
|
|
209
|
+
## Workflow Tips
|
|
210
|
+
|
|
211
|
+
1. **Initialize First**: Always run `wralph init` before using other commands
|
|
212
|
+
2. **Start Clean**: Ensure you don't have uncommitted changes before running `wralph plan` (or commit/stash them first)
|
|
213
|
+
3. **Review Plans Carefully**: The plan review step is your opportunity to catch issues before code changes
|
|
214
|
+
4. **Monitor Output**: Commands provide colored output (ℹ info, ✓ success, ⚠ warning, ✗ error) to track progress
|
|
215
|
+
5. **PR Linking**: PRs automatically reference the GitHub issue (e.g., "Fixes #123") for proper linking
|
|
216
|
+
6. **CI Timeout**: CI monitoring waits up to 1 hour for builds to complete
|
|
217
|
+
7. **Max Retries**: If CI fails more than 10 times, the command exits and requires manual intervention
|
|
218
|
+
|
|
219
|
+
## Example Session
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# 1. Initialize WRALPH in your repository
|
|
223
|
+
wralph init
|
|
224
|
+
|
|
225
|
+
# 2. Create a GitHub issue describing the bug/feature
|
|
226
|
+
|
|
227
|
+
# 3. Generate and execute plan:
|
|
228
|
+
wralph plan 456
|
|
229
|
+
|
|
230
|
+
# Review the generated plan at .wralph/plans/plan_456.md
|
|
231
|
+
# Answer any questions Claude asked, then approve
|
|
232
|
+
|
|
233
|
+
# 4. Command automatically:
|
|
234
|
+
# - Executes the plan
|
|
235
|
+
# - Creates PR #789
|
|
236
|
+
# - Monitors CI
|
|
237
|
+
# - Fixes failures if needed
|
|
238
|
+
|
|
239
|
+
# 5. Review the PR on GitHub
|
|
240
|
+
|
|
241
|
+
# 6. If changes are needed:
|
|
242
|
+
wralph feedback 456
|
|
243
|
+
# Enter your feedback, press Enter 3 times
|
|
244
|
+
|
|
245
|
+
# 7. After merging, clean up:
|
|
246
|
+
wralph remove 456
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Error Handling
|
|
250
|
+
|
|
251
|
+
The tool includes error handling for common scenarios:
|
|
252
|
+
- WRALPH not initialized (must run `wralph init` first)
|
|
253
|
+
- Missing GitHub authentication
|
|
254
|
+
- Uncommitted changes (with warning)
|
|
255
|
+
- Duplicate branches (must delete first)
|
|
256
|
+
- Missing plan files
|
|
257
|
+
- CI timeout or max retries exceeded
|
|
258
|
+
- Missing required tools
|
|
259
|
+
|
|
260
|
+
All errors provide clear messages about what went wrong and how to resolve the issue.
|
|
261
|
+
|
|
262
|
+
## Development
|
|
263
|
+
|
|
264
|
+
### Requirements
|
|
265
|
+
|
|
266
|
+
- Ruby 3.0 or higher (specified in `.ruby-version` and `Gemfile`)
|
|
267
|
+
- Development dependencies installed via `bundle install`
|
|
268
|
+
|
|
269
|
+
### Running Tests
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
bundle exec rspec
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Tests are located in the `spec/` directory and are excluded from distribution packages.
|
data/bin/wralph
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Add lib directory to load path
|
|
5
|
+
lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
6
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
7
|
+
|
|
8
|
+
require 'wralph'
|
|
9
|
+
|
|
10
|
+
# Parse arguments
|
|
11
|
+
case ARGV[0]
|
|
12
|
+
when 'init'
|
|
13
|
+
Wralph::Run::Init.run
|
|
14
|
+
when 'plan'
|
|
15
|
+
if ARGV[1].nil?
|
|
16
|
+
puts "Usage: wralph plan <github_issue_number>"
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
Wralph::Run::Plan.run(ARGV[1])
|
|
20
|
+
when 'execute'
|
|
21
|
+
if ARGV[1].nil?
|
|
22
|
+
puts "Usage: wralph execute <github_issue_number>"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
Wralph::Run::ExecutePlan.run(ARGV[1])
|
|
26
|
+
when 'feedback'
|
|
27
|
+
if ARGV[1].nil?
|
|
28
|
+
puts "Usage: wralph feedback <github_issue_number>"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
Wralph::Run::Feedback.run(ARGV[1])
|
|
32
|
+
when 'remove'
|
|
33
|
+
if ARGV[1].nil?
|
|
34
|
+
puts "Usage: wralph remove <github_issue_number>"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
Wralph::Run::Remove.run(ARGV[1])
|
|
38
|
+
when '--version', '-v'
|
|
39
|
+
puts "wralph #{Wralph::VERSION}"
|
|
40
|
+
else
|
|
41
|
+
if ARGV.empty? || ARGV[0] == '--help' || ARGV[0] == '-h'
|
|
42
|
+
puts "Usage: wralph <command> [options]"
|
|
43
|
+
puts ""
|
|
44
|
+
puts "Commands:"
|
|
45
|
+
puts " init Initialize WRALPH in the current repository"
|
|
46
|
+
puts " plan <issue_number> Generate and execute plan for GitHub issue"
|
|
47
|
+
puts " execute <issue_number> Execute an existing plan for GitHub issue"
|
|
48
|
+
puts " feedback <issue_number> Handle PR review feedback"
|
|
49
|
+
puts " remove <issue_number> Remove branch, worktree, and remote branch for issue"
|
|
50
|
+
puts ""
|
|
51
|
+
puts "Options:"
|
|
52
|
+
puts " --version, -v Show version"
|
|
53
|
+
puts " --help, -h Show this help message"
|
|
54
|
+
exit 0
|
|
55
|
+
else
|
|
56
|
+
puts "Unknown command: #{ARGV[0]}"
|
|
57
|
+
puts "Run 'wralph --help' for usage information"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require_relative '../../interfaces/shell'
|
|
5
|
+
|
|
6
|
+
module Wralph
|
|
7
|
+
module Adapters
|
|
8
|
+
module Agents
|
|
9
|
+
module ClaudeCode
|
|
10
|
+
def self.run(instructions)
|
|
11
|
+
claude_output, = Interfaces::Shell.run_command(
|
|
12
|
+
"claude -p #{Shellwords.shellescape(instructions)} --dangerously-skip-permissions", raise_on_error: false
|
|
13
|
+
)
|
|
14
|
+
claude_output
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require_relative '../../interfaces/shell'
|
|
7
|
+
require_relative '../../interfaces/print'
|
|
8
|
+
|
|
9
|
+
module Wralph
|
|
10
|
+
module Adapters
|
|
11
|
+
module Cis
|
|
12
|
+
module CircleCi
|
|
13
|
+
def self.http_get(url, api_token: nil)
|
|
14
|
+
uri = URI(url)
|
|
15
|
+
req = Net::HTTP::Get.new(uri)
|
|
16
|
+
req['Circle-Token'] = api_token if api_token
|
|
17
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(req) }
|
|
18
|
+
[res.body, res.is_a?(Net::HTTPSuccess)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get CircleCI build status for a PR
|
|
22
|
+
def self.build_status(pr_number, repo_owner, repo_name, api_token, verbose: true)
|
|
23
|
+
unless api_token
|
|
24
|
+
Interfaces::Print.error 'ci_api_token is not set in .wralph/secrets.yaml'
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get the branch name from the PR
|
|
29
|
+
branch_name, = Interfaces::Shell.run_command("gh pr view #{pr_number} --json headRefName -q .headRefName")
|
|
30
|
+
branch_name = branch_name.strip
|
|
31
|
+
Interfaces::Print.info "Checking CircleCI build for branch: #{branch_name}" if verbose && $stderr.respond_to?(:puts)
|
|
32
|
+
|
|
33
|
+
# Get the pipeline for this branch
|
|
34
|
+
project_slug = "gh/#{repo_owner}/#{repo_name}"
|
|
35
|
+
pipeline_url = "https://circleci.com/api/v2/project/#{project_slug}/pipeline?branch=#{branch_name}"
|
|
36
|
+
|
|
37
|
+
stdout, success = http_get(pipeline_url, api_token: api_token)
|
|
38
|
+
unless success
|
|
39
|
+
Interfaces::Print.warning "No CircleCI pipeline found for branch #{branch_name}" if verbose
|
|
40
|
+
return 'not_found'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
pipeline_data = JSON.parse(stdout)
|
|
45
|
+
pipeline_item = pipeline_data['items']&.first
|
|
46
|
+
return 'not_found' unless pipeline_item
|
|
47
|
+
|
|
48
|
+
pipeline_id = pipeline_item['id']
|
|
49
|
+
pipeline_state = pipeline_item['state']
|
|
50
|
+
|
|
51
|
+
Interfaces::Print.info "Pipeline ID: #{pipeline_id}, State: #{pipeline_state}" if verbose
|
|
52
|
+
|
|
53
|
+
return 'running' if %w[running pending].include?(pipeline_state)
|
|
54
|
+
|
|
55
|
+
# Get workflow details to check if it succeeded
|
|
56
|
+
workflow_url = "https://circleci.com/api/v2/pipeline/#{pipeline_id}/workflow"
|
|
57
|
+
stdout, success = http_get(workflow_url, api_token: api_token)
|
|
58
|
+
unless success
|
|
59
|
+
Interfaces::Print.warning "No workflow found for pipeline #{pipeline_id}" if verbose
|
|
60
|
+
return 'unknown'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
workflow_data = JSON.parse(stdout)
|
|
64
|
+
workflow_item = workflow_data['items']&.first
|
|
65
|
+
unless workflow_item
|
|
66
|
+
Interfaces::Print.warning "No workflow found for pipeline #{pipeline_id}" if verbose
|
|
67
|
+
return 'unknown'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
workflow_status = workflow_item['status']&.strip
|
|
71
|
+
Interfaces::Print.info "Workflow status: #{workflow_status}" if verbose
|
|
72
|
+
|
|
73
|
+
workflow_status
|
|
74
|
+
rescue JSON::ParserError => e
|
|
75
|
+
Interfaces::Print.warning "Failed to parse JSON response: #{e.message}" if verbose
|
|
76
|
+
'unknown'
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Wait for CircleCI build to complete
|
|
81
|
+
def self.wait_for_build(pr_number, repo_owner, repo_name, api_token)
|
|
82
|
+
max_wait_time = 3600 # 1 hour max wait
|
|
83
|
+
wait_interval = 30 # Check every 30 seconds
|
|
84
|
+
elapsed = 0
|
|
85
|
+
|
|
86
|
+
Interfaces::Print.info 'Waiting for CircleCI build to complete...'
|
|
87
|
+
|
|
88
|
+
last_status = nil
|
|
89
|
+
while elapsed < max_wait_time
|
|
90
|
+
# Get status quietly first to check if it changed
|
|
91
|
+
status = build_status(pr_number, repo_owner, repo_name, api_token, verbose: false)
|
|
92
|
+
|
|
93
|
+
# If status hasn't changed since last check, just print a dot
|
|
94
|
+
if last_status && last_status == status
|
|
95
|
+
print '.'
|
|
96
|
+
sleep wait_interval
|
|
97
|
+
elapsed += wait_interval
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Status changed (or first check) - get verbose output with pipeline/workflow details
|
|
102
|
+
status = build_status(pr_number, repo_owner, repo_name, api_token, verbose: true)
|
|
103
|
+
|
|
104
|
+
case status
|
|
105
|
+
when 'success'
|
|
106
|
+
Interfaces::Print.success 'CircleCI build passed!'
|
|
107
|
+
return true
|
|
108
|
+
when 'failed', 'error', 'canceled', 'unauthorized'
|
|
109
|
+
Interfaces::Print.warning "CircleCI build failed with status: #{status}"
|
|
110
|
+
return false
|
|
111
|
+
when 'running', 'on_hold'
|
|
112
|
+
Interfaces::Print.info "Build still running... (elapsed: #{elapsed}s)"
|
|
113
|
+
when 'not_found'
|
|
114
|
+
Interfaces::Print.warning 'Build not found yet, waiting...'
|
|
115
|
+
else
|
|
116
|
+
Interfaces::Print.warning "Unknown build status: #{status}, waiting..."
|
|
117
|
+
end
|
|
118
|
+
sleep wait_interval
|
|
119
|
+
elapsed += wait_interval
|
|
120
|
+
last_status = status
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Interfaces::Print.error 'Timeout waiting for CircleCI build to complete'
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get CircleCI build failures
|
|
128
|
+
def self.build_failures(pr_number, repo_owner, repo_name, api_token)
|
|
129
|
+
return 'ci_api_token is not set in .wralph/secrets.yaml' unless api_token
|
|
130
|
+
|
|
131
|
+
# 1. Get branch name from PR
|
|
132
|
+
branch_name, = Interfaces::Shell.run_command("gh pr view #{pr_number} --json headRefName -q .headRefName")
|
|
133
|
+
branch_name = branch_name.strip
|
|
134
|
+
|
|
135
|
+
# 2. Get the latest pipeline for that branch
|
|
136
|
+
project_slug = "gh/#{repo_owner}/#{repo_name}"
|
|
137
|
+
pipeline_url = "https://circleci.com/api/v2/project/#{project_slug}/pipeline?branch=#{branch_name}"
|
|
138
|
+
stdout, success = http_get(pipeline_url, api_token: api_token)
|
|
139
|
+
return 'Could not fetch pipeline' unless success
|
|
140
|
+
|
|
141
|
+
pipeline_id = JSON.parse(stdout)['items']&.first&.[]('id')
|
|
142
|
+
return 'No pipeline found' unless pipeline_id
|
|
143
|
+
|
|
144
|
+
# 3. Get the workflow ID
|
|
145
|
+
workflow_url = "https://circleci.com/api/v2/pipeline/#{pipeline_id}/workflow"
|
|
146
|
+
stdout, = http_get(workflow_url, api_token: api_token)
|
|
147
|
+
workflow_id = JSON.parse(stdout)['items']&.first&.[]('id')
|
|
148
|
+
return 'No workflow found' unless workflow_id
|
|
149
|
+
|
|
150
|
+
# 4. Get jobs and filter for failures
|
|
151
|
+
jobs_url = "https://circleci.com/api/v2/workflow/#{workflow_id}/job"
|
|
152
|
+
stdout, = http_get(jobs_url, api_token: api_token)
|
|
153
|
+
jobs_data = JSON.parse(stdout)
|
|
154
|
+
|
|
155
|
+
failed_jobs = jobs_data['items']&.select { |j| %w[failed error].include?(j['status']) }
|
|
156
|
+
return "All jobs passed for branch #{branch_name}." if failed_jobs.nil? || failed_jobs.empty?
|
|
157
|
+
|
|
158
|
+
# 5. For each failed job, reach into v1.1 API to get the logs
|
|
159
|
+
failed_jobs.map do |job|
|
|
160
|
+
job_num = job['job_number']
|
|
161
|
+
job_name = job['name']
|
|
162
|
+
|
|
163
|
+
# We use v1.1 because v2 does not provide step-level output URLs
|
|
164
|
+
v1_api_url = "https://circleci.com/api/v1.1/project/github/#{repo_owner}/#{repo_name}/#{job_num}"
|
|
165
|
+
v1_stdout, v1_success = http_get(v1_api_url, api_token: api_token)
|
|
166
|
+
|
|
167
|
+
log_content = "Job: #{job_name} (##{job_num}) failed."
|
|
168
|
+
|
|
169
|
+
if v1_success
|
|
170
|
+
job_details = JSON.parse(v1_stdout)
|
|
171
|
+
# Find the step that actually failed
|
|
172
|
+
failed_step = job_details['steps']&.find { |s| s['actions'].to_a.any? { |a| a['failed'] } }
|
|
173
|
+
|
|
174
|
+
if failed_step
|
|
175
|
+
action = failed_step['actions'].find { |a| a['failed'] }
|
|
176
|
+
output_url = action&.[]('output_url')
|
|
177
|
+
|
|
178
|
+
if output_url
|
|
179
|
+
# The output_url provides a JSON array of log lines
|
|
180
|
+
raw_log_json, = http_get(output_url)
|
|
181
|
+
begin
|
|
182
|
+
logs = JSON.parse(raw_log_json)
|
|
183
|
+
# Join the last 30 lines of the log for context
|
|
184
|
+
tail_logs = logs.map { |l| l['message'] }.last(30).join("\n")
|
|
185
|
+
log_content += "\nFAILED STEP: #{failed_step['name']}\n\nLOG TAIL:\n#{tail_logs}"
|
|
186
|
+
rescue StandardError
|
|
187
|
+
log_content += "\n(Could not parse raw log output)"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
log_content
|
|
194
|
+
end.join("\n\n#{'=' * 40}\n\n")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
require_relative '../../interfaces/shell'
|
|
6
|
+
require_relative '../../interfaces/repo'
|
|
7
|
+
|
|
8
|
+
module Wralph
|
|
9
|
+
module Adapters
|
|
10
|
+
module ObjectiveRepositories
|
|
11
|
+
module GithubIssues
|
|
12
|
+
def self.download!(identifier)
|
|
13
|
+
# Ensure objectives directory exists
|
|
14
|
+
objectives_dir = File.join(Interfaces::Repo.wralph_dir, 'objectives')
|
|
15
|
+
FileUtils.mkdir_p(objectives_dir)
|
|
16
|
+
|
|
17
|
+
# Get the local file path
|
|
18
|
+
file_path = local_file_path(identifier)
|
|
19
|
+
|
|
20
|
+
# Fetch GitHub issue content
|
|
21
|
+
issue_content, stderr, success = Interfaces::Shell.run_command("gh issue view #{Shellwords.shellescape(identifier)}")
|
|
22
|
+
raise "Failed to download GitHub issue ##{identifier}: #{stderr}" unless success
|
|
23
|
+
|
|
24
|
+
# Write the issue content to the file (overwrites if exists)
|
|
25
|
+
File.write(file_path, issue_content)
|
|
26
|
+
|
|
27
|
+
file_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.local_file_path(identifier)
|
|
31
|
+
objectives_dir = File.join(Interfaces::Repo.wralph_dir, 'objectives')
|
|
32
|
+
File.join(objectives_dir, "#{identifier}.md")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|