From 73d44f21cf99871c1a9147cf993eaa3c82ad04d2 Mon Sep 17 00:00:00 2001 From: James Johns <192176108+jamesjohnsdev@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:56:48 +1000 Subject: [PATCH 1/2] fix: unchecked errors --- cmd/util_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/util_test.go b/cmd/util_test.go index 0e87669..6fa4245 100644 --- a/cmd/util_test.go +++ b/cmd/util_test.go @@ -93,12 +93,16 @@ func injectStdin(t *testing.T, content string) { if _, err := w.WriteString(content); err != nil { t.Fatal(err) } - w.Close() + if err := w.Close(); err != nil { + t.Fatal(err) + } old := os.Stdin os.Stdin = r t.Cleanup(func() { os.Stdin = old - r.Close() + if err := r.Close(); err != nil { + t.Error(err) + } }) } From 954f89881270a51a987fa90cdb882fbb81beef83 Mon Sep 17 00:00:00 2001 From: James Johns <192176108+jamesjohnsdev@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:58:07 +1000 Subject: [PATCH 2/2] feat(create): show full schema and body marker in editor When opening a new issue in the editor, the file previously only contained title and state. Users had no way to discover or set labels, assignees, or milestone without reading the docs. Closes #7 --- cmd/create.go | 10 +++++-- internal/issue/issue.go | 47 +++++++++++++++++++++++++++++ internal/issue/issue_test.go | 58 ++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index edea29c..b9ed4e9 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -137,8 +137,14 @@ var createCmd = &cobra.Command{ path := filepath.Join(openDir(root), filename) - if err := issue.Write(path, iss); err != nil { - return err + var writeErr error + if openEditor { + writeErr = issue.WriteTemplate(path, iss) + } else { + writeErr = issue.Write(path, iss) + } + if writeErr != nil { + return writeErr } if openEditor { diff --git a/internal/issue/issue.go b/internal/issue/issue.go index a7849ea..36e93c2 100644 --- a/internal/issue/issue.go +++ b/internal/issue/issue.go @@ -68,6 +68,53 @@ func Write(path string, iss *Issue) error { return os.WriteFile(path, buf.Bytes(), 0644) } +// WriteTemplate writes an issue file with all user-editable fields expanded +// (even when empty) and a comment marking where the body begins. Used when +// opening a new issue in the editor so the user sees the full schema. +func WriteTemplate(path string, iss *Issue) error { + type tpl struct { + Title string `yaml:"title"` + State string `yaml:"state"` + Labels []string `yaml:"labels"` + Assignees []string `yaml:"assignees"` + Milestone string `yaml:"milestone"` + } + labels := iss.Labels + if labels == nil { + labels = []string{} + } + assignees := iss.Assignees + if assignees == nil { + assignees = []string{} + } + t := tpl{ + Title: iss.Title, + State: iss.State, + Labels: labels, + Assignees: assignees, + Milestone: iss.Milestone, + } + var buf bytes.Buffer + buf.WriteString("---\n") + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + if err := enc.Encode(t); err != nil { + return err + } + _ = enc.Close() + buf.WriteString("# Body goes below this line\n") + buf.WriteString("---\n") + if iss.Body != "" { + buf.WriteString("\n") + body := iss.Body + if !strings.HasSuffix(body, "\n") { + body += "\n" + } + buf.WriteString(body) + } + return os.WriteFile(path, buf.Bytes(), 0644) +} + func Filename(iss *Issue) string { return fmt.Sprintf("%d-%s.md", iss.Number, Slug(iss.Title)) } diff --git a/internal/issue/issue_test.go b/internal/issue/issue_test.go index 00e262e..f05ec04 100644 --- a/internal/issue/issue_test.go +++ b/internal/issue/issue_test.go @@ -3,6 +3,7 @@ package issue import ( "os" "path/filepath" + "strings" "testing" ) @@ -209,6 +210,63 @@ func TestWriteParse(t *testing.T) { } } +func TestWriteTemplate(t *testing.T) { + t.Run("empty issue shows all fields and separator", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "issue.md") + iss := &Issue{State: "open"} + if err := WriteTemplate(path, iss); err != nil { + t.Fatalf("WriteTemplate: %v", err) + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + content := string(raw) + for _, want := range []string{"title:", "state:", "labels:", "assignees:", "milestone:", "# Body goes below this line"} { + if !strings.Contains(content, want) { + t.Errorf("template missing %q\ngot:\n%s", want, content) + } + } + }) + + t.Run("parses back with empty body", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "issue.md") + iss := &Issue{Title: "Draft", State: "open"} + if err := WriteTemplate(path, iss); err != nil { + t.Fatalf("WriteTemplate: %v", err) + } + got, err := Parse(path) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if got.Title != "Draft" { + t.Errorf("Title = %q, want %q", got.Title, "Draft") + } + if got.Body != "" { + t.Errorf("Body = %q, want empty", got.Body) + } + }) + + t.Run("preserves populated fields", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "issue.md") + iss := &Issue{ + Title: "My Issue", + State: "open", + Labels: []string{"bug", "urgent"}, + Assignees: []string{"alice"}, + Milestone: "v1.0", + } + if err := WriteTemplate(path, iss); err != nil { + t.Fatalf("WriteTemplate: %v", err) + } + got, err := Parse(path) + if err != nil { + t.Fatalf("Parse: %v", err) + } + checkIssue(t, got, iss) + }) +} + func TestFilename(t *testing.T) { tests := []struct { name string