Skip to content

Commit c13b6e6

Browse files
authored
Merge branch 'main' into feature/tempo-plugin-8883
2 parents 0a4c26d + ad20227 commit c13b6e6

81 files changed

Lines changed: 7100 additions & 182 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/codespell.yml renamed to .github/workflows/codespell.yml.action-blocked-by-asf

File renamed without changes.

backend/plugins/bitbucket/api/connection_api_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,22 +206,22 @@ func TestMergeFromRequest_HandlesUsesApiToken(t *testing.T) {
206206
// After merge, UsesApiToken should be updated
207207
// This is a structural test - actual merge logic is in the connection.go MergeFromRequest method
208208
assert.True(t, connection.UsesApiToken, "Initial value should be true")
209-
209+
210210
// If we were to apply the merge:
211211
connection.UsesApiToken = newValues["usesApiToken"].(bool)
212212
connection.Username = newValues["username"].(string)
213-
213+
214214
assert.False(t, connection.UsesApiToken, "After merge, should be false")
215215
assert.Equal(t, "new_username", connection.Username)
216216
}
217217

218218
func TestConnectionStatusCodes(t *testing.T) {
219219
// Test expected status code handling
220220
tests := []struct {
221-
name string
222-
statusCode int
223-
expectedError bool
224-
errorType string
221+
name string
222+
statusCode int
223+
expectedError bool
224+
errorType string
225225
}{
226226
{
227227
name: "Success - 200 OK",

backend/plugins/bitbucket/api/remote_api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ func listBitbucketWorkspaces(
8080
if err != nil {
8181
return
8282
}
83+
if res.StatusCode > 299 {
84+
body, e := io.ReadAll(res.Body)
85+
if e != nil {
86+
err = errors.BadInput.Wrap(e, "failed to read response body")
87+
return
88+
}
89+
err = errors.HttpStatus(res.StatusCode).New(string(body))
90+
return
91+
}
8392

8493
resBody := &models.WorkspaceResponse{}
8594
err = api.UnmarshalResponse(res, resBody)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
coreModels "github.com/apache/incubator-devlake/core/models"
23+
"github.com/apache/incubator-devlake/core/plugin"
24+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
25+
"github.com/apache/incubator-devlake/helpers/srvhelper"
26+
"github.com/apache/incubator-devlake/plugins/claude_code/models"
27+
"github.com/apache/incubator-devlake/plugins/claude_code/tasks"
28+
)
29+
30+
// MakeDataSourcePipelinePlanV200 generates the pipeline plan for blueprint v2.0.0.
31+
func MakeDataSourcePipelinePlanV200(
32+
subtaskMetas []plugin.SubTaskMeta,
33+
connectionId uint64,
34+
bpScopes []*coreModels.BlueprintScope,
35+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
36+
_, err := dsHelper.ConnSrv.FindByPk(connectionId)
37+
if err != nil {
38+
return nil, nil, err
39+
}
40+
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
41+
if err != nil {
42+
return nil, nil, err
43+
}
44+
45+
plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails)
46+
if err != nil {
47+
return nil, nil, err
48+
}
49+
50+
return plan, nil, nil
51+
}
52+
53+
func makeDataSourcePipelinePlanV200(
54+
subtaskMetas []plugin.SubTaskMeta,
55+
scopeDetails []*srvhelper.ScopeDetail[models.ClaudeCodeScope, models.ClaudeCodeScopeConfig],
56+
) (coreModels.PipelinePlan, errors.Error) {
57+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
58+
for i, scopeDetail := range scopeDetails {
59+
stage := plan[i]
60+
if stage == nil {
61+
stage = coreModels.PipelineStage{}
62+
}
63+
64+
scope := scopeDetail.Scope
65+
task, err := helper.MakePipelinePlanTask(
66+
"claude_code",
67+
subtaskMetas,
68+
nil,
69+
tasks.ClaudeCodeOptions{
70+
ConnectionId: scope.ConnectionId,
71+
ScopeId: scope.Id,
72+
},
73+
)
74+
if err != nil {
75+
return nil, err
76+
}
77+
stage = append(stage, task)
78+
plan[i] = stage
79+
}
80+
return plan, nil
81+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"strings"
22+
23+
"github.com/apache/incubator-devlake/core/errors"
24+
"github.com/apache/incubator-devlake/core/plugin"
25+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
26+
"github.com/apache/incubator-devlake/plugins/claude_code/models"
27+
)
28+
29+
// PostConnections creates a new Claude Code connection.
30+
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
31+
connection := &models.ClaudeCodeConnection{}
32+
if err := helper.Decode(input.Body, connection, vld); err != nil {
33+
return nil, err
34+
}
35+
36+
connection.Normalize()
37+
if err := validateConnection(connection); err != nil {
38+
return nil, err
39+
}
40+
41+
if err := connectionHelper.Create(connection, input); err != nil {
42+
return nil, err
43+
}
44+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
45+
}
46+
47+
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
48+
connection := &models.ClaudeCodeConnection{}
49+
if err := connectionHelper.First(connection, input.Params); err != nil {
50+
return nil, err
51+
}
52+
if err := (&models.ClaudeCodeConnection{}).MergeFromRequest(connection, input.Body); err != nil {
53+
return nil, errors.Convert(err)
54+
}
55+
connection.Normalize()
56+
if err := validateConnection(connection); err != nil {
57+
return nil, err
58+
}
59+
if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil {
60+
return nil, err
61+
}
62+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
63+
}
64+
65+
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
66+
conn := &models.ClaudeCodeConnection{}
67+
output, err := connectionHelper.Delete(conn, input)
68+
if err != nil {
69+
return output, err
70+
}
71+
output.Body = conn.Sanitize()
72+
return output, nil
73+
}
74+
75+
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
76+
var connections []models.ClaudeCodeConnection
77+
if err := connectionHelper.List(&connections); err != nil {
78+
return nil, err
79+
}
80+
for i := range connections {
81+
connections[i] = connections[i].Sanitize()
82+
}
83+
return &plugin.ApiResourceOutput{Body: connections}, nil
84+
}
85+
86+
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
87+
connection := &models.ClaudeCodeConnection{}
88+
if err := connectionHelper.First(connection, input.Params); err != nil {
89+
return nil, err
90+
}
91+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
92+
}
93+
94+
func validateConnection(connection *models.ClaudeCodeConnection) errors.Error {
95+
if connection == nil {
96+
return errors.BadInput.New("connection is required")
97+
}
98+
hasToken := strings.TrimSpace(connection.Token) != ""
99+
if connection.HasIncompleteCustomHeaders() {
100+
return errors.BadInput.New("custom headers must include both key and value")
101+
}
102+
hasCustomHeaders := connection.HasUsableCustomHeaders()
103+
if !hasToken && !hasCustomHeaders {
104+
return errors.BadInput.New("either token or at least one custom header is required")
105+
}
106+
if strings.TrimSpace(connection.Organization) == "" {
107+
return errors.BadInput.New("organization is required")
108+
}
109+
if connection.RateLimitPerHour < 0 {
110+
return errors.BadInput.New("rateLimitPerHour must be non-negative")
111+
}
112+
return nil
113+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
25+
"github.com/apache/incubator-devlake/plugins/claude_code/models"
26+
)
27+
28+
const (
29+
testOrganization = "anthropic-labs"
30+
testToken = "sk-ant-example"
31+
)
32+
33+
func TestValidateConnectionSuccess(t *testing.T) {
34+
connection := &models.ClaudeCodeConnection{
35+
ClaudeCodeConn: models.ClaudeCodeConn{
36+
Organization: testOrganization,
37+
Token: testToken,
38+
},
39+
}
40+
connection.Normalize()
41+
42+
err := validateConnection(connection)
43+
assert.NoError(t, err)
44+
}
45+
46+
func TestValidateConnectionNil(t *testing.T) {
47+
err := validateConnection(nil)
48+
assert.Error(t, err)
49+
assert.Contains(t, err.Error(), "connection is required")
50+
}
51+
52+
func TestValidateConnectionMissingOrganization(t *testing.T) {
53+
connection := &models.ClaudeCodeConnection{
54+
ClaudeCodeConn: models.ClaudeCodeConn{
55+
Token: testToken,
56+
},
57+
}
58+
59+
err := validateConnection(connection)
60+
assert.Error(t, err)
61+
assert.Contains(t, err.Error(), "organization is required")
62+
}
63+
64+
func TestValidateConnectionMissingToken(t *testing.T) {
65+
connection := &models.ClaudeCodeConnection{
66+
ClaudeCodeConn: models.ClaudeCodeConn{
67+
Organization: testOrganization,
68+
},
69+
}
70+
71+
err := validateConnection(connection)
72+
assert.Error(t, err)
73+
assert.Contains(t, err.Error(), "either token or at least one custom header is required")
74+
}
75+
76+
func TestValidateConnectionCustomHeadersWithoutToken(t *testing.T) {
77+
connection := &models.ClaudeCodeConnection{
78+
ClaudeCodeConn: models.ClaudeCodeConn{
79+
Organization: testOrganization,
80+
CustomHeaders: []models.CustomHeader{
81+
{Key: "Ocp-Apim-Subscription-Key", Value: "secret-key"},
82+
},
83+
},
84+
}
85+
connection.Normalize()
86+
87+
err := validateConnection(connection)
88+
assert.NoError(t, err)
89+
}
90+
91+
func TestValidateConnectionRejectsBlankCustomHeaders(t *testing.T) {
92+
connection := &models.ClaudeCodeConnection{
93+
ClaudeCodeConn: models.ClaudeCodeConn{
94+
Organization: testOrganization,
95+
CustomHeaders: []models.CustomHeader{
96+
{Key: "", Value: ""},
97+
},
98+
},
99+
}
100+
connection.Normalize()
101+
102+
err := validateConnection(connection)
103+
assert.Error(t, err)
104+
assert.Contains(t, err.Error(), "either token or at least one custom header is required")
105+
}
106+
107+
func TestValidateConnectionRejectsIncompleteCustomHeaders(t *testing.T) {
108+
connection := &models.ClaudeCodeConnection{
109+
ClaudeCodeConn: models.ClaudeCodeConn{
110+
Organization: testOrganization,
111+
CustomHeaders: []models.CustomHeader{
112+
{Key: "Ocp-Apim-Subscription-Key", Value: ""},
113+
},
114+
},
115+
}
116+
connection.Normalize()
117+
118+
err := validateConnection(connection)
119+
assert.Error(t, err)
120+
assert.Contains(t, err.Error(), "custom headers must include both key and value")
121+
}
122+
123+
func TestValidateConnectionInvalidRateLimit(t *testing.T) {
124+
connection := &models.ClaudeCodeConnection{
125+
ClaudeCodeConn: models.ClaudeCodeConn{
126+
Organization: testOrganization,
127+
Token: testToken,
128+
},
129+
}
130+
connection.RateLimitPerHour = -1
131+
132+
err := validateConnection(connection)
133+
assert.Error(t, err)
134+
assert.Contains(t, err.Error(), "rateLimitPerHour must be non-negative")
135+
}

0 commit comments

Comments
 (0)