05594113a2
agentpr/watchpr already propagated command errors to a non-zero exit, but that behaviour had no regression coverage and the root command was not constructible outside main(). watchpr also fired a spurious conflict alert because Gitea computes mergeability asynchronously and can briefly report mergeable=false right after a push. The docs additionally printed the AppRole role_id literal UUID. - Extract newRootCmd() in both cmd/agentpr and cmd/watchpr so main() only runs Execute and exits non-zero on error; add tests asserting Execute returns an error for a bad PR ref / malformed --repo / no args. - Debounce mergeability loss in MeaningfulChange: only alert when mergeable=false persists across two consecutive polls (both prev and cur false, still open); update the table test for one-poll-false (benign), false-persisting (alert), and recovered false->true (benign). - Refer to AGENT_APPROLE_ROLE_ID by env var in README.md/AGENTS.md without printing the literal role_id; keep the code default and env override.
174 lines
4.6 KiB
Go
174 lines
4.6 KiB
Go
// Command agentpr manages Gitea pull requests and comments as the unkin-agent
|
|
// user. It obtains a scoped Gitea token from Vault (AppRole login, then reads
|
|
// gitea/creds/unkin-agent) so actions are attributed to the agent rather than
|
|
// to whoever runs the tool.
|
|
//
|
|
// agentpr pr create --repo owner/repo --base main --head feature --title T --body B
|
|
// agentpr pr comment --repo owner/repo --pr 12 --body "..."
|
|
// agentpr whoami
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"git.unkin.net/unkin/agent-tools/internal/agent"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
// cobra prints the error itself (SilenceErrors stays off); we only need to
|
|
// turn any command error into a non-zero exit.
|
|
if err := newRootCmd().Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// newRootCmd builds the agentpr command tree. It is separated from main so
|
|
// tests can invoke Execute and assert the exit behaviour without spawning a
|
|
// process.
|
|
func newRootCmd() *cobra.Command {
|
|
root := &cobra.Command{
|
|
Use: "agentpr",
|
|
Short: "Manage Gitea PRs and comments as the unkin-agent user.",
|
|
Long: "agentpr manages Gitea pull requests and comments as unkin-agent, using a\nGitea token minted from Vault (AppRole login + gitea/creds/unkin-agent).",
|
|
Version: version,
|
|
SilenceUsage: true,
|
|
}
|
|
root.SetVersionTemplate("{{.Version}}\n")
|
|
|
|
root.AddCommand(newPRCmd(), newWhoamiCmd(), newVersionCmd())
|
|
return root
|
|
}
|
|
|
|
// client mints a Gitea token via Vault and returns a ready client.
|
|
func client() (*agent.GiteaClient, error) {
|
|
token, err := agent.GiteaToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return agent.NewGiteaClient(token), nil
|
|
}
|
|
|
|
func newPRCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "pr",
|
|
Short: "Create PRs and post PR comments",
|
|
}
|
|
cmd.AddCommand(newPRCreateCmd(), newPRCommentCmd())
|
|
return cmd
|
|
}
|
|
|
|
func newPRCreateCmd() *cobra.Command {
|
|
var repo, base, head, title, body string
|
|
cmd := &cobra.Command{
|
|
Use: "create",
|
|
Short: "Open a pull request",
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
owner, name, err := agent.ParseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if base == "" || head == "" || title == "" {
|
|
return fmt.Errorf("--base, --head and --title are required")
|
|
}
|
|
c, err := client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pr, err := c.CreatePR(owner+"/"+name, agent.CreatePROptions{
|
|
Base: base,
|
|
Head: head,
|
|
Title: title,
|
|
Body: body,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("#%d %s\n", pr.Number, pr.HTMLURL)
|
|
return nil
|
|
},
|
|
}
|
|
f := cmd.Flags()
|
|
f.StringVar(&repo, "repo", "", "Repository as owner/repo (required)")
|
|
f.StringVar(&base, "base", "", "Base branch (required)")
|
|
f.StringVar(&head, "head", "", "Head branch (required)")
|
|
f.StringVar(&title, "title", "", "PR title (required)")
|
|
f.StringVar(&body, "body", "", "PR body")
|
|
_ = cmd.MarkFlagRequired("repo")
|
|
return cmd
|
|
}
|
|
|
|
func newPRCommentCmd() *cobra.Command {
|
|
var repo, body string
|
|
var pr int
|
|
cmd := &cobra.Command{
|
|
Use: "comment",
|
|
Short: "Post a comment on a pull request",
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
owner, name, err := agent.ParseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pr <= 0 {
|
|
return fmt.Errorf("--pr must be a positive PR number")
|
|
}
|
|
if body == "" {
|
|
return fmt.Errorf("--body is required")
|
|
}
|
|
c, err := client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cm, err := c.CreateComment(owner+"/"+name, pr, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("comment %d posted on %s/%s#%d\n", cm.ID, owner, name, pr)
|
|
return nil
|
|
},
|
|
}
|
|
f := cmd.Flags()
|
|
f.StringVar(&repo, "repo", "", "Repository as owner/repo (required)")
|
|
f.IntVar(&pr, "pr", 0, "PR number (required)")
|
|
f.StringVar(&body, "body", "", "Comment body (required)")
|
|
_ = cmd.MarkFlagRequired("repo")
|
|
_ = cmd.MarkFlagRequired("pr")
|
|
_ = cmd.MarkFlagRequired("body")
|
|
return cmd
|
|
}
|
|
|
|
func newWhoamiCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "whoami",
|
|
Short: "Print the authenticated Gitea login (should be unkin-agent)",
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u, err := c.Whoami()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(u.Login)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVersionCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "version",
|
|
Short: "Print the version",
|
|
Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) },
|
|
SilenceUsage: true,
|
|
}
|
|
}
|