fix(import): defensive handling for closed issues missing closed_at timestamp
Fixes GH#523: older versions of bd could close issues without setting the closed_at timestamp. When importing such issues, validation would fail with "closed issues must have closed_at timestamp". This fix adds defensive handling in all issue creation/validation paths: - If status is "closed" and closed_at is nil, set closed_at to max(created_at, updated_at) + 1 second - Similarly for tombstones missing deleted_at Applied to: - batch_ops.go: validateBatchIssuesWithCustomStatuses (main import path) - transaction.go: CreateIssue and CreateIssues - queries.go: CreateIssue - multirepo.go: upsertIssueInTx Also adds comprehensive tests for the defensive fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,16 +50,41 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
|
||||
return fmt.Errorf("failed to get custom statuses: %w", err)
|
||||
}
|
||||
|
||||
// Set timestamps first so defensive fixes can use them
|
||||
now := time.Now()
|
||||
if issue.CreatedAt.IsZero() {
|
||||
issue.CreatedAt = now
|
||||
}
|
||||
if issue.UpdatedAt.IsZero() {
|
||||
issue.UpdatedAt = now
|
||||
}
|
||||
|
||||
// Defensive fix for closed_at invariant (GH#523): older versions of bd could
|
||||
// close issues without setting closed_at. Fix by using max(created_at, updated_at) + 1s.
|
||||
if issue.Status == types.StatusClosed && issue.ClosedAt == nil {
|
||||
maxTime := issue.CreatedAt
|
||||
if issue.UpdatedAt.After(maxTime) {
|
||||
maxTime = issue.UpdatedAt
|
||||
}
|
||||
closedAt := maxTime.Add(time.Second)
|
||||
issue.ClosedAt = &closedAt
|
||||
}
|
||||
|
||||
// Defensive fix for deleted_at invariant: tombstones must have deleted_at
|
||||
if issue.Status == types.StatusTombstone && issue.DeletedAt == nil {
|
||||
maxTime := issue.CreatedAt
|
||||
if issue.UpdatedAt.After(maxTime) {
|
||||
maxTime = issue.UpdatedAt
|
||||
}
|
||||
deletedAt := maxTime.Add(time.Second)
|
||||
issue.DeletedAt = &deletedAt
|
||||
}
|
||||
|
||||
// Validate issue before creating (with custom status support)
|
||||
if err := issue.ValidateWithCustomStatuses(customStatuses); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Set timestamps
|
||||
now := time.Now()
|
||||
issue.CreatedAt = now
|
||||
issue.UpdatedAt = now
|
||||
|
||||
// Compute content hash (bd-95)
|
||||
if issue.ContentHash == "" {
|
||||
issue.ContentHash = issue.ComputeContentHash()
|
||||
|
||||
Reference in New Issue
Block a user