feat: Add external_ref field for linking to external issue trackers

Add nullable external_ref TEXT field to link bd issues with external
systems like GitHub Issues, Jira, etc. Includes automatic schema
migration for backward compatibility.

Changes:
- Added external_ref column to issues table with feature-based migration
- Updated Issue struct with ExternalRef *string field
- Added --external-ref flag to bd create and bd update commands
- Updated all SQL queries across the codebase to include external_ref:
  - GetIssue, CreateIssue, UpdateIssue, SearchIssues
  - GetDependencies, GetDependents, GetDependencyTree
  - GetReadyWork, GetBlockedIssues, GetIssuesByLabel
- Added external_ref handling in import/export logic
- Follows existing patterns for nullable fields (sql.NullString)

This enables tracking relationships between bd issues and external
systems without requiring changes to existing databases or JSONL files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-14 02:43:10 -07:00
parent 287c3144c4
commit e6be7dd3e8
8 changed files with 105 additions and 14 deletions

View File

@@ -156,7 +156,7 @@ func (s *SQLiteStorage) GetDependencies(ctx context.Context, issueID string) ([]
rows, err := s.db.QueryContext(ctx, `
SELECT i.id, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
i.created_at, i.updated_at, i.closed_at
i.created_at, i.updated_at, i.closed_at, i.external_ref
FROM issues i
JOIN dependencies d ON i.id = d.depends_on_id
WHERE d.issue_id = ?
@@ -175,7 +175,7 @@ func (s *SQLiteStorage) GetDependents(ctx context.Context, issueID string) ([]*t
rows, err := s.db.QueryContext(ctx, `
SELECT i.id, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
i.created_at, i.updated_at, i.closed_at
i.created_at, i.updated_at, i.closed_at, i.external_ref
FROM issues i
JOIN dependencies d ON i.id = d.issue_id
WHERE d.depends_on_id = ?
@@ -267,6 +267,7 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
i.id, i.title, i.status, i.priority, i.description, i.design,
i.acceptance_criteria, i.notes, i.issue_type, i.assignee,
i.estimated_minutes, i.created_at, i.updated_at, i.closed_at,
i.external_ref,
0 as depth
FROM issues i
WHERE i.id = ?
@@ -277,6 +278,7 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
i.id, i.title, i.status, i.priority, i.description, i.design,
i.acceptance_criteria, i.notes, i.issue_type, i.assignee,
i.estimated_minutes, i.created_at, i.updated_at, i.closed_at,
i.external_ref,
t.depth + 1
FROM issues i
JOIN dependencies d ON i.id = d.depends_on_id
@@ -297,12 +299,13 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64
var assignee sql.NullString
var externalRef sql.NullString
err := rows.Scan(
&node.ID, &node.Title, &node.Status, &node.Priority,
&node.Description, &node.Design, &node.AcceptanceCriteria,
&node.Notes, &node.IssueType, &assignee, &estimatedMinutes,
&node.CreatedAt, &node.UpdatedAt, &closedAt, &node.Depth,
&node.CreatedAt, &node.UpdatedAt, &closedAt, &externalRef, &node.Depth,
)
if err != nil {
return nil, fmt.Errorf("failed to scan tree node: %w", err)
@@ -318,6 +321,9 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
if assignee.Valid {
node.Assignee = assignee.String
}
if externalRef.Valid {
node.ExternalRef = &externalRef.String
}
node.Truncated = node.Depth == maxDepth
@@ -415,12 +421,13 @@ func scanIssues(rows *sql.Rows) ([]*types.Issue, error) {
var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64
var assignee sql.NullString
var externalRef sql.NullString
err := rows.Scan(
&issue.ID, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.UpdatedAt, &closedAt,
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
)
if err != nil {
return nil, fmt.Errorf("failed to scan issue: %w", err)
@@ -436,6 +443,9 @@ func scanIssues(rows *sql.Rows) ([]*types.Issue, error) {
if assignee.Valid {
issue.Assignee = assignee.String
}
if externalRef.Valid {
issue.ExternalRef = &externalRef.String
}
issues = append(issues, &issue)
}