Add external_ref field to CreateArgs and UpdateArgs RPC protocol structs to enable linking issues to external systems (GitHub, Jira, Shortcut, etc.) when using daemon mode. Changes: - Add ExternalRef field to rpc.CreateArgs and rpc.UpdateArgs - Update bd create/update commands to pass external_ref via RPC - Update daemon handlers to process external_ref field - Add integration tests for create and update operations The --external-ref flag now works correctly in both daemon and direct modes. Fixes https://github.com/steveyegge/beads/issues/303 Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -66,6 +66,7 @@ type CreateArgs struct {
|
||||
Design string `json:"design,omitempty"`
|
||||
AcceptanceCriteria string `json:"acceptance_criteria,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
ExternalRef string `json:"external_ref,omitempty"` // Link to external issue trackers
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
}
|
||||
@@ -81,6 +82,7 @@ type UpdateArgs struct {
|
||||
AcceptanceCriteria *string `json:"acceptance_criteria,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Assignee *string `json:"assignee,omitempty"`
|
||||
ExternalRef *string `json:"external_ref,omitempty"` // Link to external issue trackers
|
||||
}
|
||||
|
||||
// CloseArgs represents arguments for the close operation
|
||||
|
||||
@@ -710,3 +710,125 @@ func TestCreate_DiscoveredFromInheritsSourceRepo(t *testing.T) {
|
||||
// The logic is implemented in server_issues_epics.go handleCreate
|
||||
// and tested via the cmd/bd test which has direct storage access
|
||||
}
|
||||
|
||||
func TestRPCCreateWithExternalRef(t *testing.T) {
|
||||
server, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create issue with external_ref via RPC
|
||||
createArgs := &CreateArgs{
|
||||
Title: "Test issue with external ref",
|
||||
Description: "Testing external_ref in daemon mode",
|
||||
IssueType: "bug",
|
||||
Priority: 1,
|
||||
ExternalRef: "github:303",
|
||||
}
|
||||
|
||||
resp, err := client.Create(createArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
var issue types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &issue); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Verify external_ref was saved
|
||||
if issue.ExternalRef == nil {
|
||||
t.Fatal("Expected ExternalRef to be set, got nil")
|
||||
}
|
||||
if *issue.ExternalRef != "github:303" {
|
||||
t.Errorf("Expected ExternalRef='github:303', got '%s'", *issue.ExternalRef)
|
||||
}
|
||||
|
||||
// Verify via Show operation
|
||||
showArgs := &ShowArgs{ID: issue.ID}
|
||||
resp, err = client.Show(showArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Show failed: %v", err)
|
||||
}
|
||||
|
||||
var retrieved types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &retrieved); err != nil {
|
||||
t.Fatalf("Failed to unmarshal show response: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.ExternalRef == nil {
|
||||
t.Fatal("Expected retrieved ExternalRef to be set, got nil")
|
||||
}
|
||||
if *retrieved.ExternalRef != "github:303" {
|
||||
t.Errorf("Expected retrieved ExternalRef='github:303', got '%s'", *retrieved.ExternalRef)
|
||||
}
|
||||
|
||||
_ = server // Silence unused warning
|
||||
}
|
||||
|
||||
func TestRPCUpdateWithExternalRef(t *testing.T) {
|
||||
server, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create issue without external_ref
|
||||
createArgs := &CreateArgs{
|
||||
Title: "Test issue for update",
|
||||
Description: "Testing external_ref update in daemon mode",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
|
||||
resp, err := client.Create(createArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
var issue types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &issue); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Update with external_ref
|
||||
newRef := "jira-ABC-123"
|
||||
updateArgs := &UpdateArgs{
|
||||
ID: issue.ID,
|
||||
ExternalRef: &newRef,
|
||||
}
|
||||
|
||||
resp, err = client.Update(updateArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
|
||||
var updated types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &updated); err != nil {
|
||||
t.Fatalf("Failed to unmarshal update response: %v", err)
|
||||
}
|
||||
|
||||
// Verify external_ref was updated
|
||||
if updated.ExternalRef == nil {
|
||||
t.Fatal("Expected ExternalRef to be set after update, got nil")
|
||||
}
|
||||
if *updated.ExternalRef != "jira-ABC-123" {
|
||||
t.Errorf("Expected ExternalRef='jira-ABC-123', got '%s'", *updated.ExternalRef)
|
||||
}
|
||||
|
||||
// Verify via Show operation
|
||||
showArgs := &ShowArgs{ID: issue.ID}
|
||||
resp, err = client.Show(showArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Show failed: %v", err)
|
||||
}
|
||||
|
||||
var retrieved types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &retrieved); err != nil {
|
||||
t.Fatalf("Failed to unmarshal show response: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.ExternalRef == nil {
|
||||
t.Fatal("Expected retrieved ExternalRef to be set, got nil")
|
||||
}
|
||||
if *retrieved.ExternalRef != "jira-ABC-123" {
|
||||
t.Errorf("Expected retrieved ExternalRef='jira-ABC-123', got '%s'", *retrieved.ExternalRef)
|
||||
}
|
||||
|
||||
_ = server // Silence unused warning
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} {
|
||||
if a.Assignee != nil {
|
||||
u["assignee"] = *a.Assignee
|
||||
}
|
||||
if a.ExternalRef != nil {
|
||||
u["external_ref"] = *a.ExternalRef
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
issueID = childID
|
||||
}
|
||||
|
||||
var design, acceptance, assignee *string
|
||||
var design, acceptance, assignee, externalRef *string
|
||||
if createArgs.Design != "" {
|
||||
design = &createArgs.Design
|
||||
}
|
||||
@@ -118,6 +121,9 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
if createArgs.Assignee != "" {
|
||||
assignee = &createArgs.Assignee
|
||||
}
|
||||
if createArgs.ExternalRef != "" {
|
||||
externalRef = &createArgs.ExternalRef
|
||||
}
|
||||
|
||||
issue := &types.Issue{
|
||||
ID: issueID,
|
||||
@@ -128,6 +134,7 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
Design: strValue(design),
|
||||
AcceptanceCriteria: strValue(acceptance),
|
||||
Assignee: strValue(assignee),
|
||||
ExternalRef: externalRef,
|
||||
Status: types.StatusOpen,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user