26cd05e961
agentpr always read gitea/creds/unkin-agent from a bare const, so a service like repospawner could not run it as its own Gitea identity. - Replace the GiteaCredsPath const with a function: GITEA_CREDS_PATH when set, otherwise gitea/creds/<AGENT_LOGIN>. Unset env still resolves to gitea/creds/unkin-agent, so existing callers are unchanged. - Thread the creds path through fetchGiteaToken/readGiteaCreds instead of reading a package-level const, and report it in the error messages. - Make agentpr's help text login-agnostic and document both variables.
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
// Command agentpr manages Gitea pull requests and comments as an agent user. It
|
|
// obtains a scoped Gitea token from Vault (AppRole login, then reads
|
|
// gitea/creds/<AGENT_LOGIN>, or GITEA_CREDS_PATH when set) so actions are
|
|
// attributed to that 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 an agent user.",
|
|
Long: "agentpr manages Gitea pull requests and comments as an agent user, using a\nGitea token minted from Vault (AppRole login + gitea/creds/<AGENT_LOGIN>).\nSet AGENT_LOGIN to act as another agent identity, or GITEA_CREDS_PATH to name\nthe Vault creds path outright.",
|
|
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 (the identity PRs are opened as)",
|
|
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,
|
|
}
|
|
}
|