Files
agent-tools/cmd/agentpr/main.go
T
unkin-agent 26cd05e961
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Make the Vault gitea creds path selectable
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.
2026-08-30 00:50:35 +10:00

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,
}
}