Skip to content

Commit

Permalink
feat: auto-cancel PipelineRuns on PR close
Browse files Browse the repository at this point in the history
The pipelinesascode.tekton.dev/cancel-in-progress: "true" feature
annotation has now been enhanced to include automatic cancellation of
PipelineRuns when the associated pull request is closed or merged.

Jira: https://issues.redhat.com/browse/SRVKP-6908
Signed-off-by: Chmouel Boudjnah <[email protected]>
  • Loading branch information
chmouel committed Jan 15, 2025
1 parent c98f3ff commit e5f70c1
Show file tree
Hide file tree
Showing 40 changed files with 625 additions and 170 deletions.
10 changes: 7 additions & 3 deletions docs/content/docs/guide/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ click on it and follow the pipeline execution directly there.
### Cancelling in-progress PipelineRuns

{{< tech_preview "Cancelling in progress PipelineRuns" >}}
{{< support_matrix github_app="true" github_webhook="true" gitea="true" gitlab="true" bitbucket_cloud="true" bitbucket_server="false" >}}

You can choose to cancel a PipelineRun that is currently in progress. This can
be done by adding the annotation `pipelinesascode.tekton.dev/cancel-in-progress:
Expand All @@ -125,9 +126,12 @@ The cancellation only applies to `PipelineRuns` within the scope of the current
cancel-in-progress annotation, only the `PipelineRun` in the specific PullRequest
will be cancelled. This prevents interference between separate PullRequests.

The cancellation of the older `PipelineRuns` will be executed only after the
latest `PipelineRun` has been created and started successfully. This annotation
cannot guarantee that only one `PipelineRun` will be active at any given time.
Older `PipelineRuns` are canceled only after the latest `PipelineRun` is
successfully created and started. This annotation does not guarantee that only
one `PipelineRun` will be active at a time.

If a `PipelineRun` is in progress and the Pull Request is closed or declined,
the `PipelineRun` will be canceled.

Currently, `cancel-in-progress` cannot be used in conjunction with the [concurrency
limit]({{< relref "/docs/guide/repositorycrd.md#concurrency" >}}) setting.
Expand Down
17 changes: 9 additions & 8 deletions docs/content/docs/install/bitbucket_cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ Check these boxes to add the permissions to the token:

- Webhooks: `Read and write`

{{< hint info >}}
[Refer to this screenshot](/images/bitbucket-cloud-create-secrete.png) to verify you have properly configured the app password.
{{< /hint >}}
[Refer to this screenshot](/images/bitbucket-cloud-create-secrete.png) to verify
you have properly configured the app password.

Keep the generated token noted somewhere, or otherwise you will have to recreate it.

Expand Down Expand Up @@ -85,19 +84,21 @@ $ tkn pac create repo

- The individual events to select are :
- Repository -> Push
- Repository -> Updated
- Repository -> Commit comment created
- Pull Request -> Created
- Pull Request -> Updated
- Pull Request -> Merged
- Pull Request -> Declined
- Pull Request -> Comment created
- Pull Request -> Comment updated

{{< hint info >}}
[Refer to this screenshot](/images/bitbucket-cloud-create-webhook.png) to verify you have properly configured the webhook.
{{< /hint >}}
[Refer to this screenshot](/images/bitbucket-cloud-create-webhook.png) to verify you have properly configured the webhook.

- Click on **Save**.
- Click on **Save**.

- You can now create a [`Repository CRD`](/docs/guide/repositorycrd).
It will have:

- A **Username** (i.e: your Bitbucket username).
- A reference to a Kubernetes **Secret** containing the App Password as generated previously for Pipelines-as-Code operations.

Expand Down
Binary file modified docs/static/images/bitbucket-cloud-create-webhook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 2 additions & 7 deletions pkg/cli/webhook/bitbucket_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli/prompt"
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud"
)

type bitbucketCloudConfig struct {
Expand Down Expand Up @@ -135,14 +136,8 @@ func (bb *bitbucketCloudConfig) create() error {
RepoSlug: bb.repoName,
Url: bb.controllerURL,
Active: true,
Events: []string{
"repo:push",
"pullrequest:created",
"pullrequest:updated",
"pullrequest:comment_created",
},
Events: bitbucketcloud.PullRequestAllEvents,
}

_, err := bb.Client.Repositories.Webhooks.Create(opts)
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions pkg/formatting/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package formatting
import (
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
corev1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis"
)

// PipelineRunStatus return status of PR success failed or skipped.
func PipelineRunStatus(pr *tektonv1.PipelineRun) string {
if len(pr.Status.Conditions) == 0 {
return "neutral"
}
if pr.Status.GetCondition(apis.ConditionSucceeded).GetReason() == tektonv1.PipelineRunSpecStatusCancelled {
return "cancelled"
}
if pr.Status.Conditions[0].Status == corev1.ConditionFalse {
return "failure"
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/formatting/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"gotest.tools/v3/assert"
corev1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis"
knativeduckv1 "knative.dev/pkg/apis/duck/v1"
)

Expand All @@ -30,6 +31,23 @@ func TestPipelineRunStatus(t *testing.T) {
},
},
},
{
name: "cancelled",
pr: &tektonv1.PipelineRun{
Status: tektonv1.PipelineRunStatus{
Status: knativeduckv1.Status{
Conditions: knativeduckv1.Conditions{
{
Status: corev1.ConditionTrue,
Reason: tektonv1.PipelineRunSpecStatusCancelled,
Message: "Cancelled",
Type: apis.ConditionSucceeded,
},
},
},
},
},
},
{
name: "failure",
pr: &tektonv1.PipelineRun{
Expand Down
15 changes: 8 additions & 7 deletions pkg/params/triggertype/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ func StringToType(s string) Trigger {
}

const (
OkToTest Trigger = "ok-to-test"
Retest Trigger = "retest"
Push Trigger = "push"
PullRequest Trigger = "pull_request"
LabelUpdate Trigger = "label_update"
Cancel Trigger = "cancel"
CheckSuiteRerequested Trigger = "check-suite-rerequested"
CheckRunRerequested Trigger = "check-run-rerequested"
Incoming Trigger = "incoming"
CheckSuiteRerequested Trigger = "check-suite-rerequested"
Comment Trigger = "comment"
Incoming Trigger = "incoming"
LabelUpdate Trigger = "label_update"
OkToTest Trigger = "ok-to-test"
PullRequestClosed Trigger = "pull_request_closed"
PullRequest Trigger = "pull_request" // it's should be "pull_request_opened_updated" but let's keep it simple.
Push Trigger = "push"
Retest Trigger = "retest"
)
107 changes: 65 additions & 42 deletions pkg/pipelineascode/cancel_pipelineruns.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,49 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
)

type matchingCond func(pr tektonv1.PipelineRun) bool

var cancelMergePatch = map[string]interface{}{
"spec": map[string]interface{}{
"status": tektonv1.PipelineRunSpecStatusCancelledRunFinally,
},
}

// cancelInProgress cancels all PipelineRuns associated with a given repository and pull request,
// cancelAllInProgressBelongingToPullRequest cancels all in-progress PipelineRuns
// that belong to a specific pull request in the given repository.
func (p *PacRun) cancelAllInProgressBelongingToPullRequest(ctx context.Context, repo *v1alpha1.Repository) error {
labelSelector := getLabelSelector(map[string]string{
keys.URLRepository: formatting.CleanValueKubernetes(p.event.Repository),
keys.PullRequest: strconv.Itoa(int(p.event.PullRequestNumber)),
})
prs, err := p.run.Clients.Tekton.TektonV1().PipelineRuns(repo.Namespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return fmt.Errorf("failed to list pipelineRuns : %w", err)
}

if len(prs.Items) == 0 {
msg := fmt.Sprintf("no pipelinerun found for repository: %v and pullRequest %v",
p.event.Repository, p.event.PullRequestNumber)
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryPipelineRun", msg)
return nil
}

p.cancelPipelineRuns(ctx, prs, repo, func(_ tektonv1.PipelineRun) bool {
return true
})

return nil
}

// cancelInProgressMatchingPR cancels all PipelineRuns associated with a given repository and pull request,
// except for the one that triggered the cancellation. It first checks if the cancellation is in progress
// and if the repository has a concurrency limit. If a concurrency limit is set, it returns an error as
// cancellation is not supported with concurrency limits. It then retrieves the original pull request name
// from the annotations and lists all PipelineRuns with matching labels. For each PipelineRun that is not
// already done, cancelled, or gracefully stopped, it patches the PipelineRun to cancel it.
func (p *PacRun) cancelInProgress(ctx context.Context, matchPR *tektonv1.PipelineRun, repo *v1alpha1.Repository) error {
func (p *PacRun) cancelInProgressMatchingPR(ctx context.Context, matchPR *tektonv1.PipelineRun, repo *v1alpha1.Repository) error {
if matchPR == nil {
return nil
}
Expand Down Expand Up @@ -67,51 +97,28 @@ func (p *PacRun) cancelInProgress(ctx context.Context, matchPR *tektonv1.Pipelin
if err != nil {
return fmt.Errorf("failed to list pipelineRuns : %w", err)
}
var wg sync.WaitGroup
for _, pr := range prs.Items {
if pr.GetName() == matchPR.GetName() {
continue
}

p.cancelPipelineRuns(ctx, prs, repo, func(pr tektonv1.PipelineRun) bool {
// skip our own for cancellation
if sourceBranch, ok := pr.GetAnnotations()[keys.SourceBranch]; ok {
// NOTE(chmouel): Every PR has their own branch and so is every push to different branch
// it means we only cancel pipelinerun of the same name that runs to
// the unique branch. Note: HeadBranch is the branch from where the PR
// comes from in git jargon.
if sourceBranch != p.event.HeadBranch {
p.logger.Infof("cancel-in-progress: skipping pipelinerun %v/%v as it is not from the same branch, annotation source-branch: %s event headbranch: %s", pr.GetNamespace(), pr.GetName(), sourceBranch, p.event.HeadBranch)
continue
return false
}
}

if pr.IsPending() {
p.logger.Infof("cancel-in-progress: skipping pipelinerun %v/%v as it is pending", pr.GetNamespace(), pr.GetName())
}

if pr.IsDone() {
p.logger.Infof("cancel-in-progress: skipping pipelinerun %v/%v as it is done", pr.GetNamespace(), pr.GetName())
continue
}
if pr.IsCancelled() || pr.IsGracefullyCancelled() || pr.IsGracefullyStopped() {
p.logger.Infof("cancel-in-progress: skipping pipelinerun %v/%v as it is already in %v state", pr.GetNamespace(), pr.GetName(), pr.Spec.Status)
continue
}

p.logger.Infof("cancel-in-progress: cancelling pipelinerun %v/%v", pr.GetNamespace(), pr.GetName())
wg.Add(1)
go func(ctx context.Context, pr tektonv1.PipelineRun) {
defer wg.Done()
if _, err := action.PatchPipelineRun(ctx, p.logger, "cancel patch", p.run.Clients.Tekton, &pr, cancelMergePatch); err != nil {
errMsg := fmt.Sprintf("failed to cancel pipelineRun %s/%s: %s", pr.GetNamespace(), pr.GetName(), err.Error())
p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "RepositoryPipelineRun", errMsg)
}
}(ctx, pr)
}
wg.Wait()

return pr.GetName() != matchPR.GetName()
})
return nil
}

func (p *PacRun) cancelPipelineRuns(ctx context.Context, repo *v1alpha1.Repository) error {
// cancelPipelineRunsOpsComment cancels all PipelineRuns associated with a given repository and pull request.
// when the user issue a cancel comment.
func (p *PacRun) cancelPipelineRunsOpsComment(ctx context.Context, repo *v1alpha1.Repository) error {
labelSelector := getLabelSelector(map[string]string{
keys.URLRepository: formatting.CleanValueKubernetes(p.event.Repository),
keys.SHA: formatting.CleanValueKubernetes(p.event.SHA),
Expand All @@ -137,22 +144,40 @@ func (p *PacRun) cancelPipelineRuns(ctx context.Context, repo *v1alpha1.Reposito
return nil
}

var wg sync.WaitGroup
for _, pr := range prs.Items {
p.cancelPipelineRuns(ctx, prs, repo, func(pr tektonv1.PipelineRun) bool {
if p.event.TargetCancelPipelineRun != "" {
if prName, ok := pr.GetAnnotations()[keys.OriginalPRName]; !ok || prName != p.event.TargetCancelPipelineRun {
continue
return false
}
}
if pr.IsDone() {
p.logger.Infof("pipelinerun %v/%v is done, skipping cancellation", pr.GetNamespace(), pr.GetName())
return true
})

return nil
}

func (p *PacRun) cancelPipelineRuns(ctx context.Context, prs *tektonv1.PipelineRunList, repo *v1alpha1.Repository, condition matchingCond) {
var wg sync.WaitGroup
for _, pr := range prs.Items {
if !condition(pr) {
continue
}

if pr.IsCancelled() || pr.IsGracefullyCancelled() || pr.IsGracefullyStopped() {
p.logger.Infof("pipelinerun %v/%v is already in %v state", pr.GetNamespace(), pr.GetName(), pr.Spec.Status)
p.logger.Infof("cancel-in-progress: skipping cancelling pipelinerun %v/%v, already in %v state", pr.GetNamespace(), pr.GetName(), pr.Spec.Status)
continue
}

if pr.IsDone() {
p.logger.Infof("cancel-in-progress: skipping cancelling pipelinerun %v/%v, already done", pr.GetNamespace(), pr.GetName())
continue
}

if pr.IsPending() {
p.logger.Infof("cancel-in-progress: skipping cancelling pipelinerun %v/%v in pending state", pr.GetNamespace(), pr.GetName())
}

p.logger.Infof("cancel-in-progress: cancelling pipelinerun %v/%v", pr.GetNamespace(), pr.GetName())
wg.Add(1)
go func(ctx context.Context, pr tektonv1.PipelineRun) {
defer wg.Done()
Expand All @@ -163,8 +188,6 @@ func (p *PacRun) cancelPipelineRuns(ctx context.Context, repo *v1alpha1.Reposito
}(ctx, pr)
}
wg.Wait()

return nil
}

func getLabelSelector(labelsMap map[string]string) string {
Expand Down
Loading

0 comments on commit e5f70c1

Please sign in to comment.