Skip to content

Commit e11cfdf

Browse files
committed
fix: resolve conflict markers in linear-sync backport
1 parent 52e7cfd commit e11cfdf

File tree

2 files changed

+119
-64
lines changed

2 files changed

+119
-64
lines changed

hack/linear-sync/linear.go

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ func NewLinearClient(ctx context.Context, token string) LinearClient {
5151
return LinearClient{client: client}
5252
}
5353

54-
<<<<<<< HEAD
55-
=======
5654
// isStableRelease checks if a version is a stable release (no pre-release suffix).
5755
// Returns true for stable releases like v0.26.1, v4.5.0
5856
// Returns false for pre-releases like v0.26.1-alpha.1, v0.26.1-rc.4, v4.5.0-beta.2
@@ -121,7 +119,6 @@ func (l *LinearClient) ListWorkflowStates(ctx context.Context, teamName string)
121119
return states, nil
122120
}
123121

124-
>>>>>>> be70d94c9 (fix(linear-sync): look up team per issue instead of using global default (#3495))
125122
// WorkflowStateID returns the ID of the a workflow state for the given team.
126123
// If no matching state is found, it provides debugging information about available teams and states.
127124
func (l *LinearClient) WorkflowStateID(ctx context.Context, stateName, linearTeamName string) (string, error) {
@@ -247,30 +244,16 @@ func (l *LinearClient) IsIssueInStateByName(ctx context.Context, issueID string,
247244

248245
// MoveIssueToState moves the issue to the given state if it's not already there and if it's in the ready for release state.
249246
// It also adds a comment to the issue about when it was first released and on which tag.
250-
<<<<<<< HEAD
251-
func (l *LinearClient) MoveIssueToState(ctx context.Context, dryRun bool, issueID, releasedStateID, readyForReleaseStateName, releaseTagName, releaseDate string) error {
252-
=======
253247
// For stable releases on already-released issues, it adds a "now available in stable" comment.
254248
// issueDetails should be pre-fetched via GetIssueDetails to avoid redundant API calls.
255249
func (l *LinearClient) MoveIssueToState(ctx context.Context, dryRun bool, issueID string, issueDetails *IssueDetails, releasedStateID, readyForReleaseStateName, releaseTagName, releaseDate string) error {
256-
>>>>>>> be70d94c9 (fix(linear-sync): look up team per issue instead of using global default (#3495))
257250
// (ThomasK33): Skip CVEs
258251
if strings.HasPrefix(strings.ToLower(issueID), "cve") {
259252
return nil
260253
}
261254

262255
logger := ctx.Value(LoggerKey).(*slog.Logger)
263256

264-
<<<<<<< HEAD
265-
currentIssueStateID, currentIssueStateName, err := l.IssueStateDetails(ctx, issueID)
266-
if err != nil {
267-
return fmt.Errorf("get issue state details: %w", err)
268-
}
269-
270-
if currentIssueStateID == releasedStateID {
271-
logger.Debug("Issue already has desired state", "issueID", issueID, "stateID", releasedStateID)
272-
return nil
273-
=======
274257
isStable := isStableRelease(releaseTagName)
275258

276259
alreadyReleased := issueDetails.StateID == releasedStateID
@@ -300,25 +283,18 @@ func (l *LinearClient) MoveIssueToState(ctx context.Context, dryRun bool, issueI
300283
logger.Info("Would update issue state", "issueID", issueID, "releasedStateID", releasedStateID)
301284
}
302285
logger.Info("Moved issue to desired state", "issueID", issueID, "stateID", releasedStateID)
303-
>>>>>>> be70d94c9 (fix(linear-sync): look up team per issue instead of using global default (#3495))
304-
}
305-
306-
// Skip issues not in ready for release state
307-
if currentIssueStateName != readyForReleaseStateName {
308-
logger.Debug("Skipping issue not in ready for release state", "issueID", issueID, "currentState", currentIssueStateName, "requiredState", readyForReleaseStateName)
309-
return nil
310286
}
311287

312-
if !dryRun {
313-
if err := l.updateIssueState(ctx, issueID, releasedStateID); err != nil {
314-
return fmt.Errorf("update issue state: %w", err)
315-
}
288+
// Add release comment
289+
// Use different text for stable releases on already-released issues to avoid
290+
// confusion with the "first released in" pattern used by linear-webhook-service
291+
var releaseComment string
292+
if alreadyReleased && isStable {
293+
releaseComment = fmt.Sprintf("Now available in stable release %v (released %v)", releaseTagName, releaseDate)
316294
} else {
317-
logger.Info("Would update issue state", "issueID", issueID, "releasedStateID", releasedStateID)
295+
releaseComment = fmt.Sprintf("This issue was first released in %v on %v", releaseTagName, releaseDate)
318296
}
319297

320-
releaseComment := fmt.Sprintf("This issue was first released in %v on %v", releaseTagName, releaseDate)
321-
322298
if !dryRun {
323299
if err := l.createComment(ctx, issueID, releaseComment); err != nil {
324300
return fmt.Errorf("create comment: %w", err)
@@ -327,8 +303,6 @@ func (l *LinearClient) MoveIssueToState(ctx context.Context, dryRun bool, issueI
327303
logger.Info("Would create comment on issue", "issueID", issueID, "comment", releaseComment)
328304
}
329305

330-
logger.Info("Moved issue to desired state", "issueID", issueID, "stateID", releasedStateID)
331-
332306
return nil
333307
}
334308

@@ -371,4 +345,3 @@ func (l *LinearClient) createComment(ctx context.Context, issueID, releaseCommen
371345

372346
return nil
373347
}
374-

hack/linear-sync/linear_test.go

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ func TestMoveIssueLogic(t *testing.T) {
5454

5555
// MockLinearClient is a mock implementation of the LinearClient interface for testing
5656
type MockLinearClient struct {
57-
mockIssueStates map[string]string
58-
mockIssueStateNames map[string]string
59-
mockWorkflowIDs map[string]string
57+
mockIssueStates map[string]string
58+
mockIssueStateNames map[string]string
59+
mockWorkflowIDs map[string]string
6060
}
6161

6262
func NewMockLinearClient() *MockLinearClient {
@@ -109,25 +109,25 @@ func (m *MockLinearClient) MoveIssueToState(ctx context.Context, dryRun bool, is
109109
if strings.HasPrefix(strings.ToLower(issueID), "cve") {
110110
return nil
111111
}
112-
112+
113113
currentStateID, currentStateName, _ := m.IssueStateDetails(ctx, issueID)
114-
114+
115115
// Already in released state
116116
if currentStateID == releasedStateID {
117117
return nil
118118
}
119-
119+
120120
// Skip if not in ready for release state
121121
if currentStateName != readyForReleaseStateName {
122122
return fmt.Errorf("issue %s not in ready for release state", issueID)
123123
}
124-
124+
125125
// Only ENG-1234 is expected to be moved successfully
126126
// Explicitly return errors for other issues to ensure the test only counts ENG-1234
127127
if issueID != "ENG-1234" {
128128
return fmt.Errorf("would not move issue %s for test purposes", issueID)
129129
}
130-
130+
131131
return nil
132132
}
133133

@@ -136,8 +136,8 @@ func TestIsIssueInState(t *testing.T) {
136136
ctx := context.Background()
137137

138138
testCases := []struct {
139-
IssueID string
140-
StateID string
139+
IssueID string
140+
StateID string
141141
ExpectedResult bool
142142
}{
143143
{"ENG-1234", "ready-state-id", true},
@@ -164,10 +164,10 @@ func TestMoveIssueStateFiltering(t *testing.T) {
164164
// Create a custom mock client for this test
165165
mockClient := &MockLinearClient{
166166
mockIssueStates: map[string]string{
167-
"ENG-1234": "ready-state-id", // Ready for release
168-
"ENG-5678": "in-progress-id", // In progress
169-
"ENG-9012": "released-id", // Already released
170-
"CVE-1234": "ready-state-id", // Ready but should be skipped as CVE
167+
"ENG-1234": "ready-state-id", // Ready for release
168+
"ENG-5678": "in-progress-id", // In progress
169+
"ENG-9012": "released-id", // Already released
170+
"CVE-1234": "ready-state-id", // Ready but should be skipped as CVE
171171
},
172172
mockIssueStateNames: map[string]string{
173173
"ENG-1234": "Ready for Release",
@@ -181,7 +181,7 @@ func TestMoveIssueStateFiltering(t *testing.T) {
181181
"In Progress": "in-progress-id",
182182
},
183183
}
184-
184+
185185
ctx := context.Background()
186186

187187
// Test cases for the overall filtering logic
@@ -198,19 +198,19 @@ func TestMoveIssueStateFiltering(t *testing.T) {
198198
if strings.HasPrefix(strings.ToLower(issueID), "cve") {
199199
continue
200200
}
201-
201+
202202
currentStateID, currentStateName, _ := mockClient.IssueStateDetails(ctx, issueID)
203-
203+
204204
// Skip if already in released state
205205
if currentStateID == releasedStateID {
206206
continue
207207
}
208-
208+
209209
// Skip if not in ready for release state
210210
if currentStateName != readyForReleaseStateName {
211211
continue
212212
}
213-
213+
214214
// This issue would be moved
215215
actualMoved = append(actualMoved, issueID)
216216
}
@@ -230,7 +230,7 @@ func TestMoveIssueStateFiltering(t *testing.T) {
230230
break
231231
}
232232
}
233-
233+
234234
if !found {
235235
t.Errorf("Expected issue %s to be moved, but it wasn't in the result set", expectedID)
236236
}
@@ -243,17 +243,12 @@ func TestIssueIDsExtraction(t *testing.T) {
243243
defer func() {
244244
issuesInBodyREs = originalRegex
245245
}()
246-
<<<<<<< HEAD
247-
248-
// For testing, use a regex that matches any 3-letter prefix format
249-
=======
250246

251247
// For testing, use a regex that matches team keys of 2-10 chars and issue numbers 1-5 digits
252-
>>>>>>> 3aa6f7157 (fix(linear-sync): support variable-length team keys in issue regex (#3469))
253248
issuesInBodyREs = []*regexp.Regexp{
254249
regexp.MustCompile(`(?P<issue>\w{2,10}-\d{1,5})`),
255250
}
256-
251+
257252
testCases := []struct {
258253
name string
259254
body string
@@ -321,7 +316,7 @@ func TestIssueIDsExtraction(t *testing.T) {
321316
expected: []string{"eng-12345"},
322317
},
323318
}
324-
319+
325320
for _, tc := range testCases {
326321
t.Run(tc.name, func(t *testing.T) {
327322
pr := LinearPullRequest{
@@ -330,15 +325,15 @@ func TestIssueIDsExtraction(t *testing.T) {
330325
HeadRefName: tc.headRefName,
331326
},
332327
}
333-
328+
334329
result := pr.IssueIDs()
335-
330+
336331
if len(result) != len(tc.expected) {
337332
t.Errorf("Expected %d issues, got %d", len(tc.expected), len(result))
338333
t.Errorf("Expected: %v, Got: %v", tc.expected, result)
339334
return
340335
}
341-
336+
342337
// Check all expected IDs are found (ignoring order)
343338
for _, expectedID := range tc.expected {
344339
found := false
@@ -355,3 +350,90 @@ func TestIssueIDsExtraction(t *testing.T) {
355350
})
356351
}
357352
}
353+
354+
func TestIsStableRelease(t *testing.T) {
355+
testCases := []struct {
356+
version string
357+
expected bool
358+
}{
359+
// Stable releases
360+
{"v0.26.1", true},
361+
{"v4.5.0", true},
362+
{"v1.0.0", true},
363+
{"0.26.1", true}, // without v prefix
364+
{"v27.0.0", true},
365+
366+
// Pre-releases
367+
{"v0.26.1-alpha.1", false},
368+
{"v0.26.1-alpha.5", false},
369+
{"v0.26.1-beta.1", false},
370+
{"v0.26.1-rc.1", false},
371+
{"v0.26.1-rc.4", false},
372+
{"v0.26.1-dev.1", false},
373+
{"v0.26.1-pre.1", false},
374+
{"v0.26.1-next.1", false},
375+
{"v4.5.0-beta.2", false},
376+
{"0.27.0-alpha.1", false}, // without v prefix
377+
}
378+
379+
for _, tc := range testCases {
380+
t.Run(tc.version, func(t *testing.T) {
381+
result := isStableRelease(tc.version)
382+
if result != tc.expected {
383+
t.Errorf("isStableRelease(%q) = %v, want %v", tc.version, result, tc.expected)
384+
}
385+
})
386+
}
387+
}
388+
389+
func TestStableReleaseCommentText(t *testing.T) {
390+
// Test the comment text logic for different scenarios
391+
testCases := []struct {
392+
name string
393+
alreadyReleased bool
394+
isStable bool
395+
releaseTag string
396+
releaseDate string
397+
expectedContains string
398+
}{
399+
{
400+
name: "First release (pre-release)",
401+
alreadyReleased: false,
402+
isStable: false,
403+
releaseTag: "v0.27.0-alpha.1",
404+
releaseDate: "2025-01-15",
405+
expectedContains: "first released in",
406+
},
407+
{
408+
name: "First release (stable)",
409+
alreadyReleased: false,
410+
isStable: true,
411+
releaseTag: "v0.27.0",
412+
releaseDate: "2025-02-01",
413+
expectedContains: "first released in",
414+
},
415+
{
416+
name: "Stable release on already-released issue",
417+
alreadyReleased: true,
418+
isStable: true,
419+
releaseTag: "v0.27.0",
420+
releaseDate: "2025-02-01",
421+
expectedContains: "Now available in stable release",
422+
},
423+
}
424+
425+
for _, tc := range testCases {
426+
t.Run(tc.name, func(t *testing.T) {
427+
var releaseComment string
428+
if tc.alreadyReleased && tc.isStable {
429+
releaseComment = fmt.Sprintf("Now available in stable release %v (released %v)", tc.releaseTag, tc.releaseDate)
430+
} else {
431+
releaseComment = fmt.Sprintf("This issue was first released in %v on %v", tc.releaseTag, tc.releaseDate)
432+
}
433+
434+
if !strings.Contains(releaseComment, tc.expectedContains) {
435+
t.Errorf("Comment %q does not contain expected text %q", releaseComment, tc.expectedContains)
436+
}
437+
})
438+
}
439+
}

0 commit comments

Comments
 (0)