Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ go build -o github2jira .
go install .
```

There is no Makefile, goreleaser, or CI pipeline. Standard Go tooling only.
The repository uses standard Go tooling. There is no Makefile.

Release builds are configured through `.goreleaser.yaml`, and GitHub Actions
workflows under `.github/workflows/` run CI and tagged releases.

## Testing

Expand All @@ -56,7 +59,8 @@ go vet ./...
golangci-lint run ./...
```

No `.golangci.yml` config file; default rules apply.
Use `golangci-lint` for local linting alongside `go vet`. CI currently enforces
`go vet` and `go test`.

## Configuration

Expand All @@ -77,6 +81,7 @@ jira:
statuses:
with_pr: "In Development"
without_pr: "Ready"
merged_pr: "Done"
abandoned: "Abandoned"
users: # optional GitHub login -> JIRA user mapping
github-login: "jira-email@company.com"
Expand Down
99 changes: 76 additions & 23 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package cmd

import (
"context"
"fmt"
"io"
"log"
"os"
"strings"
Expand All @@ -19,8 +21,29 @@ var version = "dev"
var (
dryRun bool
debug bool

userConfigDir = os.UserConfigDir
loadConfig = config.Load
newGitHub = func(token string) githubClient {
return github.NewClient(token)
}
newJira = func(url, user, token string) (jiraClient, error) {
return jira.NewClient(url, user, token)
}
)

type githubClient interface {
FetchIssue(ctx context.Context, owner, repo string, number int) (*github.IssueInfo, error)
FetchPullRequest(ctx context.Context, owner, repo string, number int) (*github.PRInfo, error)
}

type jiraClient interface {
ResolveUser(query string) (jira.ResolvedUser, error)
FindExisting(project, repo string, number int, urls []string) ([]jira.FindResult, error)
UpdateDescription(issueKey, description string) error
CreateIssue(params jira.CreateParams) (*jira.CreatedIssue, error)
}

func init() {
rootCmd.Version = version
rootCmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "n", false,
Expand Down Expand Up @@ -82,13 +105,37 @@ func formatLink(l ghLink) string {
return fmt.Sprintf("Issue: %s", smartLink(l.URL))
}

// lineWriter wraps an io.Writer and remembers the first write error,
// turning subsequent writes into no-ops. This avoids repetitive
// per-line error checks when printing CLI output.
type lineWriter struct {
w io.Writer
err error
}

func (lw *lineWriter) printf(format string, args ...any) {
if lw.err != nil {
return
}
_, lw.err = fmt.Fprintf(lw.w, format+"\n", args...)
}

func (lw *lineWriter) writeErr() error {
if lw.err == nil {
return nil
}
return fmt.Errorf("writing output: %w", lw.err)
}

func run(cmd *cobra.Command, args []string) error {
configDir, err := os.UserConfigDir()
out := &lineWriter{w: cmd.OutOrStdout()}

configDir, err := userConfigDir()
if err != nil {
return fmt.Errorf("finding config directory: %w", err)
}

cfg, err := config.Load(configDir)
cfg, err := loadConfig(configDir)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
Expand All @@ -100,7 +147,7 @@ func run(cmd *cobra.Command, args []string) error {

debugf("parsed URL: %s/%s %s #%d", parsed.Owner, parsed.Repo, parsed.Kind, parsed.Number)

ghClient := github.NewClient(cfg.GitHub.Token)
ghClient := newGitHub(cfg.GitHub.Token)

var (
summary string
Expand Down Expand Up @@ -206,7 +253,7 @@ func run(cmd *cobra.Command, args []string) error {
issueType := issuetype.Detect(labels, prTitle)
debugf("detected issue type: %s", issueType)

jiraClient, err := jira.NewClient(cfg.Jira.URL, cfg.Jira.User, cfg.Jira.Token)
jiraClient, err := newJira(cfg.Jira.URL, cfg.Jira.User, cfg.Jira.Token)
if err != nil {
return fmt.Errorf("creating JIRA client: %w", err)
}
Expand Down Expand Up @@ -267,17 +314,17 @@ func run(cmd *cobra.Command, args []string) error {
}

if dryRun {
fmt.Println("mode: dry-run")
out.printf("mode: dry-run")
} else {
fmt.Println("mode: update")
out.printf("mode: update")
}

for _, e := range workingSet {
fmt.Printf("existing: %s %s\n", e.Key, e.URL)
fmt.Printf("summary: %s (unchanged)\n", e.Summary)
fmt.Printf("status: %s (unchanged)\n", e.Status)
out.printf("existing: %s %s", e.Key, e.URL)
out.printf("summary: %s (unchanged)", e.Summary)
out.printf("status: %s (unchanged)", e.Status)
if e.Assignee != "" {
fmt.Printf("assignee: %s (unchanged)\n", e.Assignee)
out.printf("assignee: %s (unchanged)", e.Assignee)
}
}

Expand All @@ -301,9 +348,12 @@ func run(cmd *cobra.Command, args []string) error {
}
updatedDesc := e.Description + "\n" + strings.Join(newParts, "\n")

fmt.Println("adding missing links:")
out.printf("adding missing links:")
for _, l := range missingLinks {
fmt.Println(" ", l.URL)
out.printf(" %s", l.URL)
}
if err := out.writeErr(); err != nil {
return err
}
if !dryRun {
if err := jiraClient.UpdateDescription(e.Key, updatedDesc); err != nil {
Expand All @@ -312,7 +362,7 @@ func run(cmd *cobra.Command, args []string) error {
}
}
}
return nil
return out.writeErr()
}

// No existing issue found — create a new one
Expand All @@ -329,20 +379,23 @@ func run(cmd *cobra.Command, args []string) error {
debugf("target status: %s (prState=%s)", targetStatus, prState)

if dryRun {
fmt.Println("mode: dry-run")
out.printf("mode: dry-run")
} else {
fmt.Println("mode: create")
out.printf("mode: create")
}

fmt.Printf("project: %s\n", cfg.Jira.Project)
fmt.Printf("type: %s\n", issueType)
fmt.Printf("summary: %s\n", summary)
fmt.Printf("description: %s\n", description)
out.printf("project: %s", cfg.Jira.Project)
out.printf("type: %s", issueType)
out.printf("summary: %s", summary)
out.printf("description: %s", description)
if assignee.AccountID != "" {
fmt.Printf("assignee: %s\n", assignee.DisplayName)
out.printf("assignee: %s", assignee.DisplayName)
}
fmt.Printf("transition to: %s\n", targetStatus)
out.printf("transition to: %s", targetStatus)

if err := out.writeErr(); err != nil {
return err
}
if dryRun {
return nil
}
Expand All @@ -360,7 +413,7 @@ func run(cmd *cobra.Command, args []string) error {
return fmt.Errorf("creating JIRA issue: %w", err)
}

fmt.Printf("created: %s %s\n", created.Key, created.URL)
out.printf("created: %s %s", created.Key, created.URL)

return nil
return out.err
}
Loading