Skip to content

Commit fa2a595

Browse files
committed
fix: filter out successful pipelineruns for /ok-to-test /retest
When running /ok-to-test or /retest all the pipelineruns are rerun over again. These pipeline runs instead should be skipped. When parsing for pipelineruns created for a Repository, we check whether any of the matched pipelineruns have been triggered by either the ok-to-test or retest event types. If they have, then we filter them out of the matched pipelineruns.
1 parent c2b17f0 commit fa2a595

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed

pkg/pipelineascode/match.go

+90
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/openshift-pipelines/pipelines-as-code/pkg/templates"
1919
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
2020
"go.uber.org/zap"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122
)
2223

2324
func (p *PacRun) matchRepoPR(ctx context.Context) ([]matcher.Match, *v1alpha1.Repository, error) {
@@ -165,6 +166,90 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
165166
return repo, nil
166167
}
167168

169+
// isAlreadyExecuted checks if the pipeline has already been executed for the given SHA and PR number.
170+
func (p *PacRun) isAlreadyExecuted(ctx context.Context, match matcher.Match) (bool, error) {
171+
// Get existing PipelineRuns for this repository that match the current SHA and PR number
172+
labelSelector := fmt.Sprintf("%s=%s,%s=%d,%s=%s",
173+
apipac.SHA, p.event.SHA,
174+
apipac.PullRequest, p.event.PullRequestNumber,
175+
apipac.Repository, match.Repo.Name)
176+
177+
existingPRs, err := p.run.Clients.Tekton.TektonV1().PipelineRuns(match.Repo.Namespace).List(ctx, metav1.ListOptions{
178+
LabelSelector: labelSelector,
179+
})
180+
if err != nil {
181+
return false, fmt.Errorf("failed to get existing pipelineruns: %w", err)
182+
}
183+
184+
// check for any successful runs for this specific pipeline
185+
targetPRName := strings.TrimSuffix(match.PipelineRun.GetGenerateName(), "-")
186+
if targetPRName == "" {
187+
targetPRName = match.PipelineRun.GetName()
188+
}
189+
190+
var latestPR *tektonv1.PipelineRun
191+
var latestTime *metav1.Time
192+
193+
// Find the latest pipeline run for this specific pipeline
194+
for i, pr := range existingPRs.Items {
195+
// Skip pipeline runs that are still running or not done
196+
if !pr.IsDone() || pr.Status.CompletionTime == nil {
197+
continue
198+
}
199+
200+
// if it's the same pipeline
201+
existingPRName := ""
202+
if originalPRName, ok := pr.GetAnnotations()[apipac.OriginalPRName]; ok {
203+
existingPRName = originalPRName
204+
} else {
205+
continue
206+
}
207+
208+
// Make sure we're looking at the correct pipeline
209+
if existingPRName == targetPRName {
210+
// First matching pipeline or pipeline with a newer completion time
211+
if latestPR == nil || pr.Status.CompletionTime.After(latestTime.Time) {
212+
latestPR = &existingPRs.Items[i]
213+
latestTime = pr.Status.CompletionTime
214+
}
215+
}
216+
}
217+
218+
// Only skip if the latest pipeline was successful
219+
if latestPR != nil && latestPR.Status.GetCondition("Succeeded").IsTrue() {
220+
msg := fmt.Sprintf("Skipping pipeline run %s as it has already completed successfully for SHA %s on PR #%d",
221+
targetPRName, p.event.SHA, p.event.PullRequestNumber)
222+
p.eventEmitter.EmitMessage(match.Repo, zap.InfoLevel, "RepositorySkippingPipelineRun", msg)
223+
return true, nil
224+
}
225+
226+
return false, nil
227+
}
228+
229+
// filterAlreadySuccessfulPipelines filters out pipeline runs that have already been executed successfully.
230+
func (p *PacRun) filterAlreadySuccessfulPipelines(ctx context.Context, matchedPRs []matcher.Match) []matcher.Match {
231+
filteredMatches := []matcher.Match{}
232+
for _, match := range matchedPRs {
233+
alreadyExecuted, err := p.isAlreadyExecuted(ctx, match)
234+
if err != nil {
235+
prName := match.PipelineRun.GetGenerateName()
236+
if prName == "" {
237+
prName = match.PipelineRun.GetName()
238+
}
239+
msg := fmt.Sprintf("Error checking if pipeline %s was already executed: %v",
240+
prName, err)
241+
p.eventEmitter.EmitMessage(match.Repo, zap.WarnLevel, "RepositoryCheckExecution", msg)
242+
filteredMatches = append(filteredMatches, match)
243+
continue
244+
}
245+
246+
if !alreadyExecuted {
247+
filteredMatches = append(filteredMatches, match)
248+
}
249+
}
250+
return filteredMatches
251+
}
252+
168253
// getPipelineRunsFromRepo fetches pipelineruns from git repository and prepare them for creation.
169254
func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Repository) ([]matcher.Match, error) {
170255
provenance := "source"
@@ -262,6 +347,11 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
262347
}
263348
return nil, nil
264349
}
350+
351+
// filter out pipelines that have already been executed successfully
352+
if p.event.EventType == opscomments.RetestAllCommentEventType.String() || p.event.EventType == opscomments.OkToTestCommentEventType.String() {
353+
matchedPRs = p.filterAlreadySuccessfulPipelines(ctx, matchedPRs)
354+
}
265355
}
266356

267357
// if the event is a comment event, but we don't have any match from the keys.OnComment then do the ACL checks again
+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//go:build e2e
2+
// +build e2e
3+
4+
package test
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/google/go-github/v70/github"
12+
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
13+
tgithub "github.com/openshift-pipelines/pipelines-as-code/test/pkg/github"
14+
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
15+
"gotest.tools/v3/assert"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
)
18+
19+
// TestGithubPullRequestRetestSkipSuccessful tests that a pipeline run is skipped when using /retest
20+
// if the pipeline has already been executed successfully
21+
func TestGithubPullRequestRetestSkipSuccessful(t *testing.T) {
22+
ctx := context.Background()
23+
g := &tgithub.PRTest{
24+
Label: "Github PullRequest Retest Skip Successful",
25+
YamlFiles: []string{"testdata/pipelinerun.yaml"}, // Use a simple pipeline
26+
}
27+
g.RunPullRequest(ctx, t)
28+
defer g.TearDown(ctx, t)
29+
30+
// Wait for the initial pipeline to complete
31+
waitOpts := twait.Opts{
32+
RepoName: g.TargetNamespace,
33+
Namespace: g.TargetNamespace,
34+
MinNumberStatus: 1,
35+
PollTimeout: twait.DefaultTimeout,
36+
TargetSHA: g.SHA,
37+
}
38+
_, err := twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts)
39+
assert.NilError(t, err)
40+
41+
// Get the initial number of PipelineRuns
42+
initialPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
43+
LabelSelector: keys.SHA + "=" + g.SHA,
44+
})
45+
assert.NilError(t, err)
46+
initialPRCount := len(initialPRs.Items)
47+
g.Cnx.Clients.Log.Infof("Found %d initial PipelineRuns", initialPRCount)
48+
49+
// Send a /retest comment
50+
g.Cnx.Clients.Log.Infof("Creating /retest comment in PullRequest")
51+
_, _, err = g.Provider.Client.Issues.CreateComment(ctx,
52+
g.Options.Organization,
53+
g.Options.Repo, g.PRNumber,
54+
&github.IssueComment{Body: github.Ptr("/retest")})
55+
assert.NilError(t, err)
56+
57+
// Allow some time for the comment to be processed
58+
time.Sleep(10 * time.Second)
59+
60+
// Get the final number of PipelineRuns
61+
finalPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
62+
LabelSelector: keys.SHA + "=" + g.SHA,
63+
})
64+
assert.NilError(t, err)
65+
finalPRCount := len(finalPRs.Items)
66+
g.Cnx.Clients.Log.Infof("Found %d final PipelineRuns", finalPRCount)
67+
68+
// Verify no new PipelineRuns were created
69+
assert.Equal(t, initialPRCount, finalPRCount, "Expected no new PipelineRuns to be created as the initial one succeeded")
70+
71+
// Check that we have logs from the controller indicating the pipeline was skipped
72+
// This is a bit tricky in E2E tests, so we'll just check that we still have the successful status
73+
repo, err := g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
74+
assert.NilError(t, err)
75+
assert.Equal(t, string(repo.Status[len(repo.Status)-1].Conditions[0].Status), "True", "Repository status should be successful")
76+
}
77+
78+
// TestGithubPullRequestRetestRunFailedPipeline tests that a pipeline run is not skipped when using /retest
79+
// if the most recent execution of the pipeline has failed (even if an earlier one succeeded)
80+
func TestGithubPullRequestRetestRunFailedPipeline(t *testing.T) {
81+
ctx := context.Background()
82+
g := &tgithub.PRTest{
83+
Label: "Github PullRequest Retest Run Failed Pipeline",
84+
YamlFiles: []string{"testdata/pipelinerun.yaml", "testdata/pipelinerun-error-snippet.yaml"}, // The second one will fail
85+
}
86+
g.RunPullRequest(ctx, t)
87+
defer g.TearDown(ctx, t)
88+
89+
// Wait for the initial pipelines to complete
90+
waitOpts := twait.Opts{
91+
RepoName: g.TargetNamespace,
92+
Namespace: g.TargetNamespace,
93+
MinNumberStatus: 1,
94+
PollTimeout: twait.DefaultTimeout,
95+
TargetSHA: g.SHA,
96+
}
97+
_, err := twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts)
98+
assert.NilError(t, err)
99+
100+
// Get the initial number of PipelineRuns
101+
initialPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
102+
LabelSelector: keys.SHA + "=" + g.SHA,
103+
})
104+
assert.NilError(t, err)
105+
initialPRCount := len(initialPRs.Items)
106+
g.Cnx.Clients.Log.Infof("Found %d initial PipelineRuns", initialPRCount)
107+
108+
// Send a /retest comment
109+
g.Cnx.Clients.Log.Infof("Creating /retest comment in PullRequest")
110+
_, _, err = g.Provider.Client.Issues.CreateComment(ctx,
111+
g.Options.Organization,
112+
g.Options.Repo, g.PRNumber,
113+
&github.IssueComment{Body: github.Ptr("/retest")})
114+
assert.NilError(t, err)
115+
116+
// Wait for new pipelines to be created
117+
newWaitOpts := twait.Opts{
118+
RepoName: g.TargetNamespace,
119+
Namespace: g.TargetNamespace,
120+
MinNumberStatus: initialPRCount + 1, // At least 1 new pipeline should be created
121+
PollTimeout: twait.DefaultTimeout,
122+
TargetSHA: g.SHA,
123+
}
124+
err = twait.UntilPipelineRunCreated(ctx, g.Cnx.Clients, newWaitOpts)
125+
assert.NilError(t, err)
126+
127+
// Get the final number of PipelineRuns
128+
finalPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
129+
LabelSelector: keys.SHA + "=" + g.SHA,
130+
})
131+
assert.NilError(t, err)
132+
finalPRCount := len(finalPRs.Items)
133+
g.Cnx.Clients.Log.Infof("Found %d final PipelineRuns", finalPRCount)
134+
135+
// Verify at least one new PipelineRun was created (the failed one should be rerun)
136+
assert.Assert(t, finalPRCount > initialPRCount, "Expected at least one new PipelineRun to be created as one of the initial ones failed")
137+
}
138+
139+
// TestGithubPullRequestOkToTestSkipSuccessful tests that a pipeline run is skipped when using /ok-to-test
140+
// if the pipeline has already been executed successfully
141+
func TestGithubPullRequestOkToTestSkipSuccessful(t *testing.T) {
142+
ctx := context.Background()
143+
g := &tgithub.PRTest{
144+
Label: "Github PullRequest Ok-to-test Skip Successful",
145+
YamlFiles: []string{"testdata/pipelinerun.yaml"}, // Use a simple pipeline
146+
}
147+
g.RunPullRequest(ctx, t)
148+
defer g.TearDown(ctx, t)
149+
150+
// Wait for the initial pipeline to complete
151+
waitOpts := twait.Opts{
152+
RepoName: g.TargetNamespace,
153+
Namespace: g.TargetNamespace,
154+
MinNumberStatus: 1,
155+
PollTimeout: twait.DefaultTimeout,
156+
TargetSHA: g.SHA,
157+
}
158+
_, err := twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts)
159+
assert.NilError(t, err)
160+
161+
// Get the initial number of PipelineRuns
162+
initialPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
163+
LabelSelector: keys.SHA + "=" + g.SHA,
164+
})
165+
assert.NilError(t, err)
166+
initialPRCount := len(initialPRs.Items)
167+
g.Cnx.Clients.Log.Infof("Found %d initial PipelineRuns", initialPRCount)
168+
169+
// Send a /ok-to-test comment
170+
g.Cnx.Clients.Log.Infof("Creating /ok-to-test comment in PullRequest")
171+
_, _, err = g.Provider.Client.Issues.CreateComment(ctx,
172+
g.Options.Organization,
173+
g.Options.Repo, g.PRNumber,
174+
&github.IssueComment{Body: github.Ptr("/ok-to-test")})
175+
assert.NilError(t, err)
176+
177+
// Allow some time for the comment to be processed
178+
time.Sleep(10 * time.Second)
179+
180+
// Get the final number of PipelineRuns
181+
finalPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
182+
LabelSelector: keys.SHA + "=" + g.SHA,
183+
})
184+
assert.NilError(t, err)
185+
finalPRCount := len(finalPRs.Items)
186+
g.Cnx.Clients.Log.Infof("Found %d final PipelineRuns", finalPRCount)
187+
188+
// Verify no new PipelineRuns were created
189+
assert.Equal(t, initialPRCount, finalPRCount, "Expected no new PipelineRuns to be created as the initial one succeeded")
190+
191+
// Check that repository status remains successful
192+
repo, err := g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
193+
assert.NilError(t, err)
194+
assert.Equal(t, string(repo.Status[len(repo.Status)-1].Conditions[0].Status), "True", "Repository status should be successful")
195+
}

0 commit comments

Comments
 (0)