Skip to content

Commit d23c330

Browse files
waveywaveszakisk
authored andcommitted
fix: /ok-to-test /retest pipelineruns should not restart successful pipelineruns
when executing /ok-to-test or /retest gitops commands, check whether the last pipelinerun created for the same SHA was successful. If the run was successful, skip new pipelinerun creation - Add checkForExistingSuccessfulPipelineRun logic to prevent creating duplicate PipelineRuns when /retest or /ok-to-test commands are used and a successful PipelineRun already exists for the same SHA - Fix cancel-in-progress flow by short-circuiting PullRequestClosed events to avoid interference with matching logic - Ensure /retest <pipelinerun-name> commands always create new PipelineRuns as explicitly requested by users - Add GetOriginalPipelineRunName helper function to eliminate duplicate logic across test files for finding PipelineRun names - Fix multiple tests that incorrectly used plain /retest expecting multiple PipelineRuns: now use /retest <name> for forced reruns - Update max-keep-runs tests to use specific PipelineRun names for proper testing of cleanup functionality - Fix timing issues in tests by adding proper waits for Repository CRD updates
1 parent bc8e716 commit d23c330

13 files changed

+382
-59
lines changed

hack/gh-workflow-ci.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ get_tests() {
7474
mapfile -t testfiles < <(find test/ -maxdepth 1 -name '*.go')
7575
ghglabre="Github|Gitlab|Bitbucket"
7676
if [[ ${target} == "providers" ]]; then
77+
# echo "TestGithubMaxKeepRuns"
7778
grep -hioP "^func Test.*(${ghglabre})(\w+)\(" "${testfiles[@]}" | sed -e 's/func[ ]*//' -e 's/($//'
78-
elif [[ ${target} == "gitea_others" ]]; then
79+
elif [[ ${target} == "gitea_others" ]]; then
80+
# echo "TestGiteaParamsOnRepoCR"
7981
grep -hioP '^func Test(\w+)\(' "${testfiles[@]}" | grep -iPv "(${ghglabre})" | sed -e 's/func[ ]*//' -e 's/($//'
8082
else
8183
echo "Invalid target: ${target}"

pkg/matcher/annotation_matcher.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1212
pacerrors "github.com/openshift-pipelines/pipelines-as-code/pkg/errors"
1313
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
14+
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
1415
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
1516
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1617
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
@@ -21,6 +22,8 @@ import (
2122
"github.com/google/cel-go/common/types"
2223
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
2324
"go.uber.org/zap"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"knative.dev/pkg/apis"
2427
)
2528

2629
const (
@@ -366,12 +369,51 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
366369
}
367370

368371
if len(matchedPRs) > 0 {
372+
if existingPR := checkForExistingSuccessfulPipelineRun(ctx, logger, cs, event, repo); existingPR != nil {
373+
return []Match{{PipelineRun: existingPR, Repo: repo}}, nil
374+
}
369375
return matchedPRs, nil
370376
}
371377

372378
return nil, fmt.Errorf("%s", buildAvailableMatchingAnnotationErr(event, pruns))
373379
}
374380

381+
// checkForExistingSuccessfulPipelineRun checks if there's an existing successful PipelineRun for the same SHA
382+
// when executing /ok-to-test or /retest gitops commands.
383+
func checkForExistingSuccessfulPipelineRun(ctx context.Context, logger *zap.SugaredLogger, cs *params.Run, event *info.Event, repo *apipac.Repository) *tektonv1.PipelineRun {
384+
// Only apply this logic to /retest (without specific PR name) and /ok-to-test commands
385+
// /retest <pipelinerun-name> should always create a new run since user explicitly requested it
386+
if (event.EventType == opscomments.RetestAllCommentEventType.String() ||
387+
event.EventType == opscomments.OkToTestCommentEventType.String()) &&
388+
event.SHA != "" {
389+
labelSelector := fmt.Sprintf("%s=%s", keys.SHA, formatting.CleanValueKubernetes(event.SHA))
390+
existingPRs, err := cs.Clients.Tekton.TektonV1().PipelineRuns(repo.GetNamespace()).List(ctx, metav1.ListOptions{
391+
LabelSelector: labelSelector,
392+
})
393+
if err != nil {
394+
logger.Errorf("failed to list existing PipelineRuns for SHA %s: %v", event.SHA, err)
395+
return nil
396+
}
397+
if len(existingPRs.Items) > 0 {
398+
var lastRun tektonv1.PipelineRun
399+
lastRun = existingPRs.Items[0]
400+
401+
for _, pr := range existingPRs.Items {
402+
if pr.CreationTimestamp.After(lastRun.CreationTimestamp.Time) {
403+
lastRun = pr
404+
}
405+
}
406+
407+
if lastRun.Status.GetCondition(apis.ConditionSucceeded).IsTrue() {
408+
logger.Infof("skipping creation of new pipelinerun for sha %s as the last pipelinerun '%s' has already succeeded",
409+
event.SHA, lastRun.Name)
410+
return &lastRun
411+
}
412+
}
413+
}
414+
return nil
415+
}
416+
375417
func buildAvailableMatchingAnnotationErr(event *info.Event, pruns []*tektonv1.PipelineRun) string {
376418
errmsg := "available annotations of the PipelineRuns annotations in .tekton/ dir:"
377419
for _, prun := range pruns {

pkg/matcher/annotation_matcher_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import (
3030
zapobserver "go.uber.org/zap/zaptest/observer"
3131
"gotest.tools/v3/assert"
3232
"gotest.tools/v3/golden"
33+
corev1 "k8s.io/api/core/v1"
3334
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+
"knative.dev/pkg/apis"
36+
knativeduckv1 "knative.dev/pkg/apis/duck/v1"
3437
rtesting "knative.dev/pkg/reconciler/testing"
3538
)
3639

@@ -2599,3 +2602,130 @@ func TestGetName(t *testing.T) {
25992602
})
26002603
}
26012604
}
2605+
2606+
func TestCheckForExistingSuccessfulPipelineRun(t *testing.T) {
2607+
ctx, _ := rtesting.SetupFakeContext(t)
2608+
logger := zap.NewExample().Sugar()
2609+
2610+
repo := &v1alpha1.Repository{
2611+
ObjectMeta: metav1.ObjectMeta{
2612+
Name: "test-repo",
2613+
Namespace: "test-ns",
2614+
},
2615+
}
2616+
2617+
// Create a successful PipelineRun
2618+
pr := &tektonv1.PipelineRun{
2619+
ObjectMeta: metav1.ObjectMeta{
2620+
Name: "test-pr",
2621+
Namespace: "test-ns",
2622+
Labels: map[string]string{
2623+
keys.SHA: "test-sha",
2624+
},
2625+
CreationTimestamp: metav1.Now(),
2626+
},
2627+
Status: tektonv1.PipelineRunStatus{
2628+
Status: knativeduckv1.Status{
2629+
Conditions: knativeduckv1.Conditions{
2630+
apis.Condition{
2631+
Type: apis.ConditionSucceeded,
2632+
Status: corev1.ConditionTrue,
2633+
},
2634+
},
2635+
},
2636+
},
2637+
}
2638+
2639+
// Create a failed PipelineRun with the same SHA but older
2640+
earlierTime := metav1.NewTime(time.Now().Add(-1 * time.Hour))
2641+
failedPR := &tektonv1.PipelineRun{
2642+
ObjectMeta: metav1.ObjectMeta{
2643+
Name: "failed-pr",
2644+
Namespace: "test-ns",
2645+
Labels: map[string]string{
2646+
keys.SHA: "test-sha",
2647+
},
2648+
CreationTimestamp: earlierTime,
2649+
},
2650+
Status: tektonv1.PipelineRunStatus{
2651+
Status: knativeduckv1.Status{
2652+
Conditions: knativeduckv1.Conditions{
2653+
apis.Condition{
2654+
Type: apis.ConditionSucceeded,
2655+
Status: corev1.ConditionFalse,
2656+
},
2657+
},
2658+
},
2659+
},
2660+
}
2661+
2662+
// Setup test clients
2663+
tdata := testclient.Data{
2664+
PipelineRuns: []*tektonv1.PipelineRun{pr, failedPR},
2665+
Repositories: []*v1alpha1.Repository{repo},
2666+
}
2667+
stdata, _ := testclient.SeedTestData(t, ctx, tdata)
2668+
2669+
cs := &params.Run{
2670+
Clients: clients.Clients{
2671+
Log: logger,
2672+
Tekton: stdata.Pipeline,
2673+
Kube: stdata.Kube,
2674+
},
2675+
}
2676+
2677+
tests := []struct {
2678+
name string
2679+
eventType string
2680+
sha string
2681+
wantPR bool
2682+
}{
2683+
{
2684+
name: "Retest command with matching SHA should find successful PR",
2685+
eventType: opscomments.RetestAllCommentEventType.String(),
2686+
sha: "test-sha",
2687+
wantPR: true,
2688+
},
2689+
{
2690+
name: "Ok-to-test command with matching SHA should find successful PR",
2691+
eventType: opscomments.OkToTestCommentEventType.String(),
2692+
sha: "test-sha",
2693+
wantPR: true,
2694+
},
2695+
{
2696+
name: "Retest command with non-matching SHA should not find PR",
2697+
eventType: opscomments.RetestAllCommentEventType.String(),
2698+
sha: "other-sha",
2699+
wantPR: false,
2700+
},
2701+
{
2702+
name: "Different event type should not find PR",
2703+
eventType: opscomments.TestAllCommentEventType.String(),
2704+
sha: "test-sha",
2705+
wantPR: false,
2706+
},
2707+
}
2708+
2709+
for _, tt := range tests {
2710+
t.Run(tt.name, func(t *testing.T) {
2711+
event := &info.Event{
2712+
EventType: tt.eventType,
2713+
SHA: tt.sha,
2714+
}
2715+
2716+
foundPR := checkForExistingSuccessfulPipelineRun(ctx, logger, cs, event, repo)
2717+
2718+
if tt.wantPR && foundPR == nil {
2719+
t.Errorf("Expected to find a successful PipelineRun, but got nil")
2720+
}
2721+
2722+
if !tt.wantPR && foundPR != nil {
2723+
t.Errorf("Expected not to find a PipelineRun, but found %s", foundPR.Name)
2724+
}
2725+
2726+
if tt.wantPR && foundPR != nil {
2727+
assert.Equal(t, "test-pr", foundPR.Name)
2728+
}
2729+
})
2730+
}
2731+
}

pkg/pipelineascode/pipelineascode.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,21 @@ func NewPacs(event *info.Event, vcx provider.Interface, run *params.Run, pacInfo
5555
}
5656

5757
func (p *PacRun) Run(ctx context.Context) error {
58-
matchedPRs, repo, err := p.matchRepoPR(ctx)
59-
if repo != nil && p.event.TriggerTarget == triggertype.PullRequestClosed {
60-
if err := p.cancelAllInProgressBelongingToClosedPullRequest(ctx, repo); err != nil {
61-
return fmt.Errorf("error cancelling in progress pipelineRuns belonging to pull request %d: %w", p.event.PullRequestNumber, err)
58+
// For PullRequestClosed events, skip matching logic and go straight to cancellation
59+
if p.event.TriggerTarget == triggertype.PullRequestClosed {
60+
repo, err := p.verifyRepoAndUser(ctx)
61+
if err != nil {
62+
return err
63+
}
64+
if repo != nil {
65+
if err := p.cancelAllInProgressBelongingToClosedPullRequest(ctx, repo); err != nil {
66+
return fmt.Errorf("error cancelling in progress pipelineRuns belonging to pull request %d: %w", p.event.PullRequestNumber, err)
67+
}
6268
}
6369
return nil
6470
}
71+
72+
matchedPRs, repo, err := p.matchRepoPR(ctx)
6573
if err != nil {
6674
createStatusErr := p.vcx.CreateStatus(ctx, p.event, provider.StatusOpts{
6775
Status: CompletedStatus,

test/gitea_gitops_commands_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,22 @@ func TestGiteaRetestAll(t *testing.T) {
192192
assert.NilError(t, secret.Create(ctx, topts.ParamsRun, map[string]string{"secret": "SHHHHHHH"}, topts.TargetNS, "pac-secret"))
193193
topts.TargetEvent = triggertype.PullRequest.String()
194194
topts.YAMLFiles = map[string]string{
195-
".tekton/pr.yaml": "testdata/pipelinerun.yaml",
196-
".tekton/nomatch.yaml": "testdata/pipelinerun-nomatch.yaml",
195+
".tekton/pr.yaml": "testdata/pipelinerun.yaml",
196+
".tekton/pr-second.yaml": "testdata/pipelinerun-clone.yaml", // Use a matching PipelineRun instead of nomatch
197197
}
198198
_, f := tgitea.TestPR(t, topts)
199199
defer f()
200-
tgitea.PostCommentOnPullRequest(t, topts, "/retest")
200+
201+
// Wait for initial PipelineRuns to complete, then get one to retest specifically
202+
time.Sleep(5 * time.Second)
203+
pipelineRunName, err := twait.GetOriginalPipelineRunName(context.Background(), topts.ParamsRun.Clients, topts.TargetNS, topts.PullRequest.Head.Sha)
204+
assert.NilError(t, err)
205+
206+
tgitea.PostCommentOnPullRequest(t, topts, "/retest "+pipelineRunName)
201207
waitOpts := twait.Opts{
202208
RepoName: topts.TargetNS,
203209
Namespace: topts.TargetNS,
204-
MinNumberStatus: 2,
210+
MinNumberStatus: 3, // 2 initial + 1 retest
205211
PollTimeout: twait.DefaultTimeout,
206212
}
207213

@@ -214,6 +220,6 @@ func TestGiteaRetestAll(t *testing.T) {
214220
rt = true
215221
}
216222
}
217-
assert.Assert(t, rt, "should have a retest all comment event in status")
218-
assert.Equal(t, len(repo.Status), 2, "should have only 2 status")
223+
assert.Assert(t, rt, "should have a retest comment event in status")
224+
assert.Equal(t, len(repo.Status), 3, "should have 3 statuses: 2 initial + 1 retest")
219225
}

test/gitea_params_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,15 @@ func TestGiteaParamsOnRepoCR(t *testing.T) {
314314
_, f := tgitea.TestPR(t, topts)
315315
defer f()
316316

317-
repo, err := topts.ParamsRun.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(topts.TargetNS).Get(context.Background(), topts.TargetNS, metav1.GetOptions{})
317+
// Wait for Repository status to be updated
318+
waitOpts := twait.Opts{
319+
RepoName: topts.TargetNS,
320+
Namespace: topts.TargetNS,
321+
MinNumberStatus: 1,
322+
PollTimeout: twait.DefaultTimeout,
323+
TargetSHA: "",
324+
}
325+
repo, err := twait.UntilRepositoryUpdated(context.Background(), topts.ParamsRun.Clients, waitOpts)
318326
assert.NilError(t, err)
319327
assert.Assert(t, len(repo.Status) != 0)
320328
assert.NilError(t,

0 commit comments

Comments
 (0)