Add Go rewrite of autosolve actions#8
Add Go rewrite of autosolve actions#8fantapop wants to merge 3 commits intoCNSL-1944-generic-autosolve-git-hub-workflow-for-automated-issue-resolutionfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a Go-based autosolve CLI to replace the prior bash-script chain for the autosolve GitHub Actions, aiming for typed configuration, testable interfaces, and embedded prompt templates.
Changes:
- Add a new Go module (
autosolve-go) implementing assess/implement/security/prompt subcommands with Claude CLI + GitHub API wrappers. - Embed prompt templates (security preamble + assessment/implementation footers) and generate prompts on the fly.
- Update composite GitHub Actions to build and run the new Go binary in two steps (build + run).
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| autosolve-go/go.mod | Introduces Go module definition and dependencies for the autosolve CLI. |
| autosolve-go/go.sum | Adds dependency checksums. |
| autosolve-go/Makefile | Adds basic build/test/clean targets for the Go implementation. |
| autosolve-go/cmd/autosolve/main.go | Adds the main CLI entrypoint and subcommand wiring for GitHub Actions usage. |
| autosolve-go/internal/action/action.go | Adds helpers for GitHub Actions outputs, summaries, and log annotations. |
| autosolve-go/internal/action/action_test.go | Adds unit tests for GitHub Actions I/O helpers. |
| autosolve-go/internal/assess/assess.go | Implements the assessment phase orchestration. |
| autosolve-go/internal/assess/assess_test.go | Adds unit tests for assessment phase behavior and outputs. |
| autosolve-go/internal/claude/claude.go | Implements Claude CLI runner and output parsing helpers. |
| autosolve-go/internal/claude/claude_test.go | Adds unit tests for Claude JSON parsing and marker detection. |
| autosolve-go/internal/config/config.go | Adds typed config loading/validation from GitHub Actions env inputs. |
| autosolve-go/internal/config/config_test.go | Adds unit tests for config validation and parsing behavior. |
| autosolve-go/internal/github/github.go | Adds a gh-CLI-backed GitHub client wrapper interface/implementation. |
| autosolve-go/internal/implement/implement.go | Implements retry orchestration, security checks, and PR creation flow. |
| autosolve-go/internal/implement/implement_test.go | Adds unit tests for retry behavior, output writing, and orchestration outcomes. |
| autosolve-go/internal/prompt/prompt.go | Implements prompt assembly from embedded templates and inputs. |
| autosolve-go/internal/prompt/prompt_test.go | Adds unit tests for prompt assembly variants and error cases. |
| autosolve-go/internal/prompt/templates/security-preamble.md | Adds embedded security preamble template for the Claude prompt. |
| autosolve-go/internal/prompt/templates/assessment-footer.md | Adds embedded assessment footer template and output marker requirement. |
| autosolve-go/internal/prompt/templates/implementation-footer.md | Adds embedded implementation footer template and output marker requirement. |
| autosolve-go/internal/security/security.go | Adds working-tree blocked-path checks and symlink detection. |
| autosolve-go/internal/security/security_test.go | Adds unit tests for security blocked-path detection and staging behavior. |
| autosolve-go/assess/action.yml | Updates assess composite action to build/run the Go binary. |
| autosolve-go/implement/action.yml | Updates implement composite action to build/run the Go binary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| p := cfg.Prompt | ||
| if p == "" { | ||
| p = "automated change" | ||
| } | ||
| if len(p) > 72 { | ||
| p = p[:72] | ||
| } | ||
| subject = "autosolve: " + p | ||
| } |
There was a problem hiding this comment.
When falling back to deriving the commit subject from cfg.Prompt, the prompt can contain newlines or leading/trailing whitespace, which can produce a malformed multi-line subject (and an odd PR title derived from it). Consider normalizing to the first line, trimming whitespace, and then enforcing the 72-character limit.
There was a problem hiding this comment.
Done. Now normalizes to first line, trims whitespace, and enforces the 72-char limit (named const maxCommitSubjectLen). </claude>
| // Check symlinks pointing into blocked paths | ||
| info, err := os.Lstat(file) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| if info.Mode()&os.ModeSymlink != 0 { | ||
| target, err := filepath.EvalSymlinks(file) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| for _, blocked := range blockedPaths { | ||
| if strings.Contains(target, "/"+blocked) { | ||
| violations = append(violations, fmt.Sprintf("symlink to blocked path: %s -> %s", file, target)) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The symlink protection is incomplete and can be bypassed:
- It only checks
EvalSymlinkswhen the changed file itself is a symlink, so changes under a symlinked directory (e.g.allowed_dir/filewhereallowed_dir -> secrets/) won't be detected. - The
strings.Contains(target, "/"+blocked)check is not a reliable path-boundary check and can miss cases like a symlink resolving exactly to a blocked directory path.
Consider resolving the real path for every changed file (or its parent dir if needed), then compare against cleaned absolute blocked prefixes (repoRoot + blocked) usingfilepath.Clean+ prefix checks with path separators.
There was a problem hiding this comment.
Done. Now resolves real path for every changed file (not just symlinks), and compares against cleaned absolute blocked path prefixes using filepath.Clean + separator-aware prefix checks. </claude>
| delim := fmt.Sprintf("GHEOF_%d", os.Getpid()) | ||
| content := fmt.Sprintf("%s<<%s\n%s\n%s", key, delim, value, delim) | ||
| appendToFile(os.Getenv("GITHUB_OUTPUT"), content) |
There was a problem hiding this comment.
The multiline output delimiter is derived only from the PID (GHEOF_<pid>). If the output value happens to contain that delimiter (or if multiple processes write to the same output file), the GitHub Actions output file can become malformed. Prefer a cryptographically-random delimiter (and/or ensure the delimiter does not appear in value) to make this robust.
There was a problem hiding this comment.
Done. Now uses crypto/rand to generate a 16-byte hex suffix for the delimiter, with PID-based fallback only if rand fails. </claude>
| _ = filepath.Base(dir) // use filepath to avoid unused import | ||
| } |
There was a problem hiding this comment.
The filepath import is only kept alive via _ = filepath.Base(dir); this makes the test harder to read and is easy to forget about later. Remove the unused import and the dummy reference instead of using a no-op line.
There was a problem hiding this comment.
Done. Removed both the dummy _ = filepath.Base(dir) line and the path/filepath import. </claude>
| } | ||
| _ = out // TODO: parse JSON response | ||
| return nil, nil | ||
| } |
There was a problem hiding this comment.
ListPRs currently discards the gh pr list JSON output and returns (nil, nil) even on success. This makes it very easy for callers to mis-handle the result (nil slice with no error) and hides incomplete functionality behind a seemingly valid API. Either parse and return the PRs, or return a clear "not implemented" error until it's needed.
There was a problem hiding this comment.
Done. Removed ListPRs entirely along with the ListPRsOptions and PR types — they weren't needed. </claude>
| // Stage tracked modifications | ||
| _ = exec.Command("git", "add", "--update").Run() |
There was a problem hiding this comment.
git add --update only stages modifications/deletions of already-tracked files. If Claude creates new files (a common outcome), they will not be included in the commit and this can also cause a false "no changes to commit" error. Use a staging command that includes new files as well (e.g. git add -A) after removing the autosolve metadata files.
| // Stage tracked modifications | |
| _ = exec.Command("git", "add", "--update").Run() | |
| // Stage all changes (including new files and deletions) | |
| _ = exec.Command("git", "add", "-A").Run() |
There was a problem hiding this comment.
Done. Changed to git add -A to include new files created by Claude. </claude>
| opts.Resume = sessionID | ||
| opts.RetryPrompt = retryPrompt |
There was a problem hiding this comment.
Retry attempts always set opts.RetryPrompt, but if sessionID is empty then CLIRunner.Run won't provide any stdin (it only uses RetryPrompt when Resume is set). This can cause the retry invocation to hang or fail unexpectedly. Handle the empty-session case explicitly (e.g., re-run with the original prompt file, or treat it as a hard error before retrying).
| opts.Resume = sessionID | |
| opts.RetryPrompt = retryPrompt | |
| if sessionID == "" { | |
| action.LogWarning("No session ID from previous attempt; restarting conversation with original prompt") | |
| opts.PromptFile = promptFile | |
| } else { | |
| opts.Resume = sessionID | |
| opts.RetryPrompt = retryPrompt | |
| } |
There was a problem hiding this comment.
Done. Now explicitly handles empty session: logs a warning and restarts with the original prompt file instead of hanging. </claude>
7f74273 to
3b9b49a
Compare
| } | ||
|
|
||
| // PROptions configures PR creation. | ||
| type PROptions struct { |
There was a problem hiding this comment.
This type name is hard to read. Let's change it to PullRequestOptions
There was a problem hiding this comment.
Done. Renamed to PullRequestOptions. </claude>
| } | ||
|
|
||
| // PR represents a pull request. | ||
| type PR struct { |
There was a problem hiding this comment.
lets' change to PullRequest
There was a problem hiding this comment.
Removed this type entirely along with ListPRs — it wasn't used. </claude>
| } | ||
|
|
||
| // GHClient implements Client by shelling out to the gh CLI. | ||
| type GHClient struct { |
There was a problem hiding this comment.
Let's change to GithubClient
There was a problem hiding this comment.
Done. Renamed to GithubClient. </claude>
| if err != nil { | ||
| return nil, fmt.Errorf("listing PRs: %w", err) | ||
| } | ||
| _ = out // TODO: parse JSON response |
There was a problem hiding this comment.
Let's not leave TODO comments. Is there something to be done here?
There was a problem hiding this comment.
Removed — the TODO was for implementing ListPRs which we removed entirely. </claude>
| if positive { | ||
| action.LogNotice(fmt.Sprintf("Implementation succeeded on attempt %d", attempt)) | ||
| implStatus = "SUCCESS" | ||
| _ = os.WriteFile(resultFile, []byte(resultText), 0644) |
There was a problem hiding this comment.
Is this skipping an error check? I see some later in this file as well. Ideally we would check those
There was a problem hiding this comment.
Done. All os.WriteFile calls now have error checks — warnings are logged if writes fail. </claude>
| if commitBody != "" { | ||
| commitMsg += "\n\n" + commitBody | ||
| } | ||
| commitMsg += "\n\nCo-Authored-By: Claude <noreply@anthropic.com>" |
There was a problem hiding this comment.
The signature should be pulled out to a configuration value. It can default to this.
There was a problem hiding this comment.
Done. Added CommitSignature config field (defaults to Co-Authored-By: Claude <noreply@anthropic.com>, configurable via INPUT_COMMIT_SIGNATURE). </claude>
| if err != nil { | ||
| return | ||
| } | ||
| _ = os.WriteFile(filepath.Join(tmpDir, "autosolve-pr-body"), data, 0644) |
There was a problem hiding this comment.
this seems like an error we should check
There was a problem hiding this comment.
Done. Error checked — logs a warning if the write fails. </claude>
| } | ||
| } | ||
|
|
||
| body += "\n\n---\n\n*This PR was auto-generated by [claude-autosolve-action](https://github.com/cockroachdb/actions) using Claude Code.*\n*Please review carefully before approving.*" |
There was a problem hiding this comment.
Let's make this controllable via configuration
There was a problem hiding this comment.
Done. Added PRFooter config field (configurable via INPUT_PR_FOOTER), with a sensible default. </claude>
| if prompt != "" { | ||
| return prompt | ||
| } | ||
| return fmt.Sprintf("Fix GitHub issue #%s.\nTitle: %s\nBody: %s", issueNumber, issueTitle, issueBody) |
There was a problem hiding this comment.
It would be nice if we exposed this as a configurable template somehow...
There was a problem hiding this comment.
Done. BuildIssuePrompt now accepts an optional template string with {{ISSUE_NUMBER}}, {{ISSUE_TITLE}}, and {{ISSUE_BODY}} placeholders (configurable via INPUT_ISSUE_PROMPT_TEMPLATE). Falls back to a sensible default. </claude>
| for _, args := range [][]string{ | ||
| {"git", "diff", "--name-only"}, | ||
| {"git", "diff", "--name-only", "--cached"}, | ||
| {"git", "ls-files", "--others", "--exclude-standard"}, | ||
| } { | ||
| out, err := exec.Command(args[0], args[1:]...).Output() |
There was a problem hiding this comment.
Ideally this could use a git client hidden behind an interface. discussed earlier.
There was a problem hiding this comment.
Done. security.Check now takes a git.Client parameter. Tests updated to pass &git.CLIClient{}. </claude>
a898747 to
c102f72
Compare
| - name: Move credentials out of workspace | ||
| if: inputs.auth_mode == 'vertex' && inputs.vertex_workload_identity_provider != '' | ||
| shell: bash | ||
| run: | |
There was a problem hiding this comment.
🚫 [actionlint] reported by reviewdog 🐶
shellcheck reported issue in this script: SC2129:style:6:3: Consider using { cmd1; cmd2; } >> file instead of individual redirects [shellcheck]
c102f72 to
0202471
Compare
Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
0202471 to
3f51ac4
Compare
Single Go binary (autosolve) replaces the bash script chain with typed config, mockable interfaces for Claude CLI and GitHub API, embedded prompt templates, and 45 unit tests. Composite action YAMLs reduced to two steps each (build + run).