feat(update): add --append-notes flag (#1304)
* feat(update): add --append-notes flag (bd-b5qu) Add --append-notes flag that appends to existing notes with a newline separator instead of overwriting. This prevents data loss in workflows where multiple steps need to add info to notes (e.g., tackle workflows). - Errors if both --notes and --append-notes specified - Handles both daemon and direct mode paths - Combines existing notes + newline + new content Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(update): add tests for --append-notes flag --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -400,6 +400,64 @@ func TestCLI_UpdateEphemeralMutualExclusion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLI_UpdateAppendNotes(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping slow CLI test in short mode")
|
||||||
|
}
|
||||||
|
tmpDir := setupCLITestDB(t)
|
||||||
|
out := runBDInProcess(t, tmpDir, "create", "Issue for append-notes test", "-p", "2", "--notes", "Original notes", "--json")
|
||||||
|
|
||||||
|
var issue map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(out), &issue)
|
||||||
|
id := issue["id"].(string)
|
||||||
|
|
||||||
|
// Test appending notes
|
||||||
|
runBDInProcess(t, tmpDir, "update", id, "--append-notes", "Appended content")
|
||||||
|
|
||||||
|
out = runBDInProcess(t, tmpDir, "show", id, "--json")
|
||||||
|
var updated []map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(out), &updated)
|
||||||
|
notes := updated[0]["notes"].(string)
|
||||||
|
if notes != "Original notes\nAppended content" {
|
||||||
|
t.Errorf("Expected 'Original notes\\nAppended content', got: %q", notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test appending to empty notes
|
||||||
|
out = runBDInProcess(t, tmpDir, "create", "Issue with empty notes", "-p", "2", "--json")
|
||||||
|
json.Unmarshal([]byte(out), &issue)
|
||||||
|
id2 := issue["id"].(string)
|
||||||
|
|
||||||
|
runBDInProcess(t, tmpDir, "update", id2, "--append-notes", "First note")
|
||||||
|
|
||||||
|
out = runBDInProcess(t, tmpDir, "show", id2, "--json")
|
||||||
|
json.Unmarshal([]byte(out), &updated)
|
||||||
|
notes = updated[0]["notes"].(string)
|
||||||
|
if notes != "First note" {
|
||||||
|
t.Errorf("Expected 'First note', got: %q", notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_UpdateAppendNotesMutualExclusion(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping slow CLI test in short mode")
|
||||||
|
}
|
||||||
|
tmpDir := setupCLITestDB(t)
|
||||||
|
out := runBDInProcess(t, tmpDir, "create", "Issue for notes mutual exclusion", "-p", "2", "--json")
|
||||||
|
|
||||||
|
var issue map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(out), &issue)
|
||||||
|
id := issue["id"].(string)
|
||||||
|
|
||||||
|
// Both --notes and --append-notes should error
|
||||||
|
_, stderr, err := runBDInProcessAllowError(t, tmpDir, "update", id, "--notes", "New notes", "--append-notes", "Appended")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error when both --notes and --append-notes specified, got none")
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "cannot specify both --notes and --append-notes") {
|
||||||
|
t.Errorf("Expected mutual exclusion error message, got: %v", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCLI_Close(t *testing.T) {
|
func TestCLI_Close(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping slow CLI test in short mode")
|
t.Skip("skipping slow CLI test in short mode")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func registerCommonIssueFlags(cmd *cobra.Command) {
|
|||||||
cmd.Flags().String("design", "", "Design notes")
|
cmd.Flags().String("design", "", "Design notes")
|
||||||
cmd.Flags().String("acceptance", "", "Acceptance criteria")
|
cmd.Flags().String("acceptance", "", "Acceptance criteria")
|
||||||
cmd.Flags().String("notes", "", "Additional notes")
|
cmd.Flags().String("notes", "", "Additional notes")
|
||||||
|
cmd.Flags().String("append-notes", "", "Append to existing notes (with newline separator)")
|
||||||
cmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
cmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,17 @@ create, update, show, or close operation).`,
|
|||||||
design, _ := cmd.Flags().GetString("design")
|
design, _ := cmd.Flags().GetString("design")
|
||||||
updates["design"] = design
|
updates["design"] = design
|
||||||
}
|
}
|
||||||
|
if cmd.Flags().Changed("notes") && cmd.Flags().Changed("append-notes") {
|
||||||
|
FatalErrorRespectJSON("cannot specify both --notes and --append-notes")
|
||||||
|
}
|
||||||
if cmd.Flags().Changed("notes") {
|
if cmd.Flags().Changed("notes") {
|
||||||
notes, _ := cmd.Flags().GetString("notes")
|
notes, _ := cmd.Flags().GetString("notes")
|
||||||
updates["notes"] = notes
|
updates["notes"] = notes
|
||||||
}
|
}
|
||||||
|
if cmd.Flags().Changed("append-notes") {
|
||||||
|
appendNotes, _ := cmd.Flags().GetString("append-notes")
|
||||||
|
updates["append_notes"] = appendNotes
|
||||||
|
}
|
||||||
if cmd.Flags().Changed("acceptance") || cmd.Flags().Changed("acceptance-criteria") {
|
if cmd.Flags().Changed("acceptance") || cmd.Flags().Changed("acceptance-criteria") {
|
||||||
var acceptanceCriteria string
|
var acceptanceCriteria string
|
||||||
if cmd.Flags().Changed("acceptance") {
|
if cmd.Flags().Changed("acceptance") {
|
||||||
@@ -243,6 +250,22 @@ create, update, show, or close operation).`,
|
|||||||
if notes, ok := updates["notes"].(string); ok {
|
if notes, ok := updates["notes"].(string); ok {
|
||||||
updateArgs.Notes = ¬es
|
updateArgs.Notes = ¬es
|
||||||
}
|
}
|
||||||
|
if appendNotes, ok := updates["append_notes"].(string); ok {
|
||||||
|
// Fetch existing issue to get current notes
|
||||||
|
showArgs := &rpc.ShowArgs{ID: id}
|
||||||
|
resp, err := daemonClient.Show(showArgs)
|
||||||
|
if err == nil {
|
||||||
|
var existingIssue types.Issue
|
||||||
|
if err := json.Unmarshal(resp.Data, &existingIssue); err == nil {
|
||||||
|
combined := existingIssue.Notes
|
||||||
|
if combined != "" {
|
||||||
|
combined += "\n"
|
||||||
|
}
|
||||||
|
combined += appendNotes
|
||||||
|
updateArgs.Notes = &combined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
||||||
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
||||||
}
|
}
|
||||||
@@ -372,10 +395,19 @@ create, update, show, or close operation).`,
|
|||||||
// Apply regular field updates if any
|
// Apply regular field updates if any
|
||||||
regularUpdates := make(map[string]interface{})
|
regularUpdates := make(map[string]interface{})
|
||||||
for k, v := range updates {
|
for k, v := range updates {
|
||||||
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
|
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" && k != "append_notes" {
|
||||||
regularUpdates[k] = v
|
regularUpdates[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle append_notes: combine existing notes with new content
|
||||||
|
if appendNotes, ok := updates["append_notes"].(string); ok {
|
||||||
|
combined := issue.Notes
|
||||||
|
if combined != "" {
|
||||||
|
combined += "\n"
|
||||||
|
}
|
||||||
|
combined += appendNotes
|
||||||
|
regularUpdates["notes"] = combined
|
||||||
|
}
|
||||||
if len(regularUpdates) > 0 {
|
if len(regularUpdates) > 0 {
|
||||||
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||||
@@ -486,10 +518,19 @@ create, update, show, or close operation).`,
|
|||||||
// Apply regular field updates if any
|
// Apply regular field updates if any
|
||||||
regularUpdates := make(map[string]interface{})
|
regularUpdates := make(map[string]interface{})
|
||||||
for k, v := range updates {
|
for k, v := range updates {
|
||||||
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
|
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" && k != "append_notes" {
|
||||||
regularUpdates[k] = v
|
regularUpdates[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle append_notes: combine existing notes with new content
|
||||||
|
if appendNotes, ok := updates["append_notes"].(string); ok {
|
||||||
|
combined := issue.Notes
|
||||||
|
if combined != "" {
|
||||||
|
combined += "\n"
|
||||||
|
}
|
||||||
|
combined += appendNotes
|
||||||
|
regularUpdates["notes"] = combined
|
||||||
|
}
|
||||||
if len(regularUpdates) > 0 {
|
if len(regularUpdates) > 0 {
|
||||||
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user