From e0fdd84e043c735f55773c2c9fc9c2bb748be83e Mon Sep 17 00:00:00 2001 From: Matan Ryngler Date: Mon, 8 Jun 2026 11:21:31 +0300 Subject: [PATCH] fix(linker): handle empty pr issue regexp Signed-off-by: Matan Ryngler --- backend/plugins/linker/impl/impl.go | 15 ++-- backend/plugins/linker/impl/impl_test.go | 73 +++++++++++++++++++ .../plugins/linker/tasks/link_pr_and_issue.go | 3 + backend/plugins/linker/tasks/task_data.go | 8 +- .../plugins/linker/tasks/task_data_test.go | 43 +++++++++++ 5 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 backend/plugins/linker/impl/impl_test.go create mode 100644 backend/plugins/linker/tasks/task_data_test.go diff --git a/backend/plugins/linker/impl/impl.go b/backend/plugins/linker/impl/impl.go index e2efb809d9d..45cab6f998b 100644 --- a/backend/plugins/linker/impl/impl.go +++ b/backend/plugins/linker/impl/impl.go @@ -20,6 +20,7 @@ package impl import ( "encoding/json" "regexp" + "strings" "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/errors" @@ -84,13 +85,11 @@ func (p Linker) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]i taskData := &tasks.LinkerTaskData{ Options: op, } - if op.PrToIssueRegexp != "" { - re, err := regexp.Compile(op.PrToIssueRegexp) - if err != nil { - return taskData, errors.Convert(err) - } - taskData.PrToIssueRegexp = re + re, compileErr := regexp.Compile(op.PrToIssueRegexp) + if compileErr != nil { + return taskData, errors.Convert(compileErr) } + taskData.PrToIssueRegexp = re return taskData, nil } @@ -109,6 +108,10 @@ func (p Linker) MakeMetricPluginPipelinePlanV200(projectName string, options jso if err != nil { return nil, errors.Default.WrapRaw(err) } + op.PrToIssueRegexp = strings.TrimSpace(op.PrToIssueRegexp) + if op.PrToIssueRegexp == "" { + return nil, nil + } plan := coreModels.PipelinePlan{ { { diff --git a/backend/plugins/linker/impl/impl_test.go b/backend/plugins/linker/impl/impl_test.go new file mode 100644 index 00000000000..7f1db0c3df2 --- /dev/null +++ b/backend/plugins/linker/impl/impl_test.go @@ -0,0 +1,73 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package impl + +import ( + "encoding/json" + "testing" + + coreModels "github.com/apache/incubator-devlake/core/models" + "github.com/stretchr/testify/assert" +) + +func TestMakeMetricPluginPipelinePlanV200SkipsEmptyRegexp(t *testing.T) { + var linker Linker + + plan, err := linker.MakeMetricPluginPipelinePlanV200("project", json.RawMessage(`{}`)) + + assert.Nil(t, err) + assert.Nil(t, plan) +} + +func TestMakeMetricPluginPipelinePlanV200BuildsPlan(t *testing.T) { + var linker Linker + options, err := json.Marshal(map[string]string{ + "prToIssueRegexp": `#(\d+)`, + }) + assert.Nil(t, err) + + plan, err := linker.MakeMetricPluginPipelinePlanV200("project", options) + + assert.Nil(t, err) + assert.Equal(t, coreModels.PipelinePlan{ + { + { + Plugin: "linker", + Options: map[string]interface{}{ + "projectName": "project", + "prToIssueRegexp": `#(\d+)`, + }, + Subtasks: []string{ + "LinkPrToIssue", + }, + }, + }, + }, plan) +} + +func TestPrepareTaskDataRejectsEmptyRegexp(t *testing.T) { + var linker Linker + + taskData, err := linker.PrepareTaskData(nil, map[string]interface{}{ + "projectName": "project", + "prToIssueRegexp": "", + }) + + assert.NotNil(t, err) + assert.Nil(t, taskData) +} diff --git a/backend/plugins/linker/tasks/link_pr_and_issue.go b/backend/plugins/linker/tasks/link_pr_and_issue.go index faeeffd716b..b26d3434657 100644 --- a/backend/plugins/linker/tasks/link_pr_and_issue.go +++ b/backend/plugins/linker/tasks/link_pr_and_issue.go @@ -74,6 +74,9 @@ func clearHistoryData(db dal.Dal, data *LinkerTaskData) errors.Error { func LinkPrToIssue(taskCtx plugin.SubTaskContext) errors.Error { db := taskCtx.GetDal() data := taskCtx.GetData().(*LinkerTaskData) + if data.PrToIssueRegexp == nil { + return errors.BadInput.New("prToIssueRegexp is required") + } if err := clearHistoryData(db, data); err != nil { return err diff --git a/backend/plugins/linker/tasks/task_data.go b/backend/plugins/linker/tasks/task_data.go index 37a37c358d7..5fbbe0b45e2 100644 --- a/backend/plugins/linker/tasks/task_data.go +++ b/backend/plugins/linker/tasks/task_data.go @@ -18,9 +18,11 @@ limitations under the License. package tasks import ( + "regexp" + "strings" + "github.com/apache/incubator-devlake/core/errors" helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" - "regexp" ) type LinkerOptions struct { @@ -39,5 +41,9 @@ func DecodeAndValidateTaskOptions(options map[string]interface{}) (*LinkerOption if err != nil { return nil, errors.Default.Wrap(err, "error decoding linker task options") } + op.PrToIssueRegexp = strings.TrimSpace(op.PrToIssueRegexp) + if op.PrToIssueRegexp == "" { + return nil, errors.BadInput.New("prToIssueRegexp is required") + } return &op, nil } diff --git a/backend/plugins/linker/tasks/task_data_test.go b/backend/plugins/linker/tasks/task_data_test.go new file mode 100644 index 00000000000..5be92ccf140 --- /dev/null +++ b/backend/plugins/linker/tasks/task_data_test.go @@ -0,0 +1,43 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tasks + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeAndValidateTaskOptionsRequiresPrToIssueRegexp(t *testing.T) { + options, err := DecodeAndValidateTaskOptions(map[string]interface{}{ + "projectName": "project", + }) + + assert.NotNil(t, err) + assert.Nil(t, options) +} + +func TestDecodeAndValidateTaskOptionsTrimsPrToIssueRegexp(t *testing.T) { + options, err := DecodeAndValidateTaskOptions(map[string]interface{}{ + "projectName": "project", + "prToIssueRegexp": " #(\\d+) ", + }) + + assert.Nil(t, err) + assert.Equal(t, "#(\\d+)", options.PrToIssueRegexp) +}