Skip to content

Commit 579fd77

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 579fd77

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

pkg/pipelineascode/match.go

Lines changed: 90 additions & 0 deletions
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
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
// testGitHubCommentSkipSuccessful is a helper function to test skipping of already successful pipelines
20+
// for different GitOps commands (retest, ok-to-test).
21+
func testGitHubCommentSkipSuccessful(t *testing.T, commandType string, label string) {
22+
ctx := context.Background()
23+
g := &tgithub.PRTest{
24+
Label: label,
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 the command comment
50+
g.Cnx.Clients.Log.Infof("Creating /%s comment in PullRequest", commandType)
51+
_, _, err = g.Provider.Client.Issues.CreateComment(ctx,
52+
g.Options.Organization,
53+
g.Options.Repo, g.PRNumber,
54+
&github.IssueComment{Body: github.Ptr("/" + commandType)})
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 repository status remains successful
72+
repo, err := g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
73+
assert.NilError(t, err)
74+
assert.Equal(t, string(repo.Status[len(repo.Status)-1].Conditions[0].Status), "True", "Repository status should be successful")
75+
}
76+
77+
// TestGithubPullRequestRetestSkipSuccessful tests that a pipeline run is skipped when using /retest,
78+
// if the pipeline has already been executed successfully.
79+
func TestGithubPullRequestRetestSkipSuccessful(t *testing.T) {
80+
testGitHubCommentSkipSuccessful(t, "retest", "Github PullRequest Retest Skip Successful")
81+
}
82+
83+
// TestGithubPullRequestOkToTestSkipSuccessful tests that a pipeline run is skipped when using /ok-to-test,
84+
// if the pipeline has already been executed successfully.
85+
func TestGithubPullRequestOkToTestSkipSuccessful(t *testing.T) {
86+
testGitHubCommentSkipSuccessful(t, "ok-to-test", "Github PullRequest Ok-to-test Skip Successful")
87+
}
88+
89+
// TestGithubPullRequestRetestRunFailedPipeline tests that a pipeline run is not skipped when using /retest,
90+
// if the most recent execution of the pipeline has failed (even if an earlier one succeeded).
91+
func TestGithubPullRequestRetestRunFailedPipeline(t *testing.T) {
92+
ctx := context.Background()
93+
g := &tgithub.PRTest{
94+
Label: "Github PullRequest Retest Run Failed Pipeline",
95+
YamlFiles: []string{"testdata/pipelinerun.yaml", "testdata/pipelinerun-error-snippet.yaml"}, // The second one will fail
96+
}
97+
g.RunPullRequest(ctx, t)
98+
defer g.TearDown(ctx, t)
99+
100+
// Wait for the initial pipelines to complete
101+
waitOpts := twait.Opts{
102+
RepoName: g.TargetNamespace,
103+
Namespace: g.TargetNamespace,
104+
MinNumberStatus: 1,
105+
PollTimeout: twait.DefaultTimeout,
106+
TargetSHA: g.SHA,
107+
}
108+
_, err := twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts)
109+
assert.NilError(t, err)
110+
111+
// Get the initial number of PipelineRuns
112+
initialPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
113+
LabelSelector: keys.SHA + "=" + g.SHA,
114+
})
115+
assert.NilError(t, err)
116+
initialPRCount := len(initialPRs.Items)
117+
g.Cnx.Clients.Log.Infof("Found %d initial PipelineRuns", initialPRCount)
118+
119+
// Send a /retest comment
120+
g.Cnx.Clients.Log.Infof("Creating /retest comment in PullRequest")
121+
_, _, err = g.Provider.Client.Issues.CreateComment(ctx,
122+
g.Options.Organization,
123+
g.Options.Repo, g.PRNumber,
124+
&github.IssueComment{Body: github.Ptr("/retest")})
125+
assert.NilError(t, err)
126+
127+
// Wait for new pipelines to be created
128+
newWaitOpts := twait.Opts{
129+
RepoName: g.TargetNamespace,
130+
Namespace: g.TargetNamespace,
131+
MinNumberStatus: initialPRCount + 1, // At least 1 new pipeline should be created
132+
PollTimeout: twait.DefaultTimeout,
133+
TargetSHA: g.SHA,
134+
}
135+
err = twait.UntilPipelineRunCreated(ctx, g.Cnx.Clients, newWaitOpts)
136+
assert.NilError(t, err)
137+
138+
// Get the final number of PipelineRuns
139+
finalPRs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
140+
LabelSelector: keys.SHA + "=" + g.SHA,
141+
})
142+
assert.NilError(t, err)
143+
finalPRCount := len(finalPRs.Items)
144+
g.Cnx.Clients.Log.Infof("Found %d final PipelineRuns", finalPRCount)
145+
146+
// Verify at least one new PipelineRun was created (the failed one should be rerun)
147+
assert.Assert(t, finalPRCount > initialPRCount, "Expected at least one new PipelineRun to be created as one of the initial ones failed")
148+
}

0 commit comments

Comments
 (0)