diff --git a/docs/content/docs/guide/running.md b/docs/content/docs/guide/running.md index 185a5bb8c..1499b5ce0 100644 --- a/docs/content/docs/guide/running.md +++ b/docs/content/docs/guide/running.md @@ -114,12 +114,12 @@ entire suite of checks once again. ![github apps rerun check](/images/github-apps-rerun-checks.png) -### Gitops command on pull or merge request +### GitOps command on pull or merge request -If you are targeting a pull or merge request you can use `GitOps` comment +If you are targeting a push, pull or merge request you can use `GitOps` comment inside your pull request, to restart all or specific Pipelines. -For example you want to restart all your pipeline you can add a comment starting +For example, you want to restart all your pipeline you can add a comment starting with `/retest` and all PipelineRun attached to that pull or merge request will be restarted : @@ -141,6 +141,58 @@ roses are red, violets are blue. pipeline are bound to flake by design. /test ``` +### GitOps command on push request + +To trigger GitOps commands in response to a push request, you can include `GitOps` +comments within your commit messages. These comments can be used to restart +either all pipelines or specific ones. Here's how it works: + +For restarting all pipeline runs: + +1. Use `/retest` or `/test` within your commit message. + +For restarting a specific pipeline run: +2. Use `/retest ` or `/test ` within your +commit message. Replace `` with the specific name of the +pipeline run you want to restart. + +**Note:** + +When executing `GitOps` commands on a commit that exists in multiple branches +within a push request, the branch with the latest commit will be used. + +This means: + +1. If a user specifies commands like `/retest` or `/test` without any argument +in a comment on a branch, the test will automatically be performed on the **main** branch. + + Examples : + 1. `/retest` + 2. `/test` + 3. `/retest ` + 4. `/test ` + +2. If the user includes a branch specification such as `/retest branch:test` or +`/test branch:test`, the test will be executed on the commit where the comment is +located, with the context of the **test** branch. + + Examples : + 1. `/retest branch:test` + 2. `/test branch:test` + 3. `/retest branch:test` + 4. `/test branch:test` + +To add `GitOps` comments to a push request, follow these steps: + +1. Go to your repository. +2. Click on the **Commits** section. +3. Choose one of the individual **Commit**. +4. Click on the line number where you want to add a `GitOps` comment, as shown in the image below: + +![GitOps Commits For Comments](/images/gitops-comments-on-commit.png) + +Please note that this feature is supported for the GitHub provider only. + ## Cancelling the PipelineRun You can cancel a running PipelineRun by commenting on the PullRequest. @@ -159,7 +211,7 @@ It seems the infra is down, so cancelling the pipelineruns. If you have multiple `PipelineRun` and you want to target a specific `PipelineRun` you can use the `/cancel` comment with the PipelineRun name -Example: +Example : ```text roses are red, violets are blue. why to run the pipeline when the infra is down. @@ -170,3 +222,41 @@ roses are red, violets are blue. why to run the pipeline when the infra is down. On GitHub App the status of the Pipeline will be set to `cancelled`. ![pipelinerun canceled](/images/pr-cancel.png) + +### Cancelling the PipelineRun on push request + +You can cancel a running PipelineRun by commenting on the commit. +Here's how you can do it. + +Example : + +1. Use `/cancel` to cancel all PipeineRuns. +2. Use `/cancel ` to cancel a specific PipeineRun + +**Note:** + +When executing `GitOps` comments on a commit that exists in multiple branches +within a push request, the branch with the latest commit will be used. + +This means: + +1. If a user specifies commands like `/cancel` +without any argument in a comment on a branch, +it will automatically target the **main** branch. + + Examples : + 1. `/cancel` + 2. `/cancel ` + +2. If the user issues a command like `/cancel branch:test`, +it will target the commit where the comment was made but use the **test** branch. + + Examples : + 1. `/cancel branch:test` + 2. `/cancel branch:test` + +In the GitHub App, the status of the Pipeline will be set to `cancelled`. + +![GitOps Commits For Comments For PipelineRun Canceled](/images/gitops-comments-on-commit-cancel.png) + +Please note that this feature is supported for the GitHub provider only. diff --git a/docs/content/docs/install/github_apps.md b/docs/content/docs/install/github_apps.md index 39462a746..3a852736f 100644 --- a/docs/content/docs/install/github_apps.md +++ b/docs/content/docs/install/github_apps.md @@ -48,6 +48,7 @@ Alternatively, you could set up manually by following the steps [here](#setup-ma * Check run * Check suite * Issue comment + * Commit comment * Pull request * Push diff --git a/docs/static/images/gitops-comments-on-commit-cancel.png b/docs/static/images/gitops-comments-on-commit-cancel.png new file mode 100644 index 000000000..9325696ef Binary files /dev/null and b/docs/static/images/gitops-comments-on-commit-cancel.png differ diff --git a/docs/static/images/gitops-comments-on-commit.png b/docs/static/images/gitops-comments-on-commit.png new file mode 100644 index 000000000..dca7770d5 Binary files /dev/null and b/docs/static/images/gitops-comments-on-commit.png differ diff --git a/pkg/cmd/tknpac/bootstrap/github.go b/pkg/cmd/tknpac/bootstrap/github.go index 16eaff2e1..305d47b06 100644 --- a/pkg/cmd/tknpac/bootstrap/github.go +++ b/pkg/cmd/tknpac/bootstrap/github.go @@ -21,6 +21,7 @@ func generateManifest(opts *bootstrapOpts) ([]byte, error) { "check_run", "check_suite", "issue_comment", + "commit_comment", "pull_request", "push", }, diff --git a/pkg/pipelineascode/cancel_pipelinerun_test.go b/pkg/pipelineascode/cancel_pipelinerun_test.go index 75d649305..01408088a 100644 --- a/pkg/pipelineascode/cancel_pipelinerun_test.go +++ b/pkg/pipelineascode/cancel_pipelinerun_test.go @@ -32,6 +32,10 @@ var ( URL: "https://github.com/fooorg/foo", }, } + fooRepoLabelsForPush = map[string]string{ + keys.URLRepository: formatting.CleanValueKubernetes("foo"), + keys.SHA: formatting.CleanValueKubernetes("foosha"), + } fooRepoLabels = map[string]string{ keys.URLRepository: formatting.CleanValueKubernetes("foo"), keys.SHA: formatting.CleanValueKubernetes("foosha"), @@ -66,12 +70,6 @@ func TestCancelPipelinerun(t *testing.T) { pipelineRuns []*pipelinev1.PipelineRun cancelledPipelineRuns map[string]bool }{ - { - name: "not a pull request event", - event: &info.Event{ - TriggerTarget: "push", - }, - }, { name: "cancel running", event: &info.Event{ @@ -203,6 +201,76 @@ func TestCancelPipelinerun(t *testing.T) { repo: fooRepo, cancelledPipelineRuns: map[string]bool{}, }, + { + name: "cancel running for push event", + event: &info.Event{ + Repository: "foo", + SHA: "foosha", + TriggerTarget: "push", + State: info.State{ + CancelPipelineRuns: true, + }, + }, + pipelineRuns: []*pipelinev1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pr-foo", + Namespace: "foo", + Labels: fooRepoLabelsForPush, + }, + Spec: pipelinev1.PipelineRunSpec{}, + }, + }, + repo: fooRepo, + cancelledPipelineRuns: map[string]bool{ + "pr-foo": true, + }, + }, + { + name: "cancel a specific run for push event", + event: &info.Event{ + Repository: "foo", + SHA: "foosha", + TriggerTarget: "push", + State: info.State{ + CancelPipelineRuns: true, + TargetCancelPipelineRun: "pr-foo-abc", + }, + }, + pipelineRuns: []*pipelinev1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pr-foo", + Namespace: "foo", + Labels: fooRepoLabelsForPush, + Annotations: fooRepoAnnotations, + }, + Spec: pipelinev1.PipelineRunSpec{}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pr-foo-abc-123", + Namespace: "foo", + Labels: fooRepoLabelsPrFooAbc, + Annotations: fooRepoAnnotationsPrFooAbc, + }, + Spec: pipelinev1.PipelineRunSpec{}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pr-foo-pqr", + Namespace: "foo", + Labels: fooRepoLabelsForPush, + Annotations: fooRepoAnnotations, + }, + Spec: pipelinev1.PipelineRunSpec{}, + }, + }, + repo: fooRepo, + cancelledPipelineRuns: map[string]bool{ + "pr-foo-abc-123": true, + }, + }, } for _, tt := range tests { diff --git a/pkg/pipelineascode/cancel_pipelineruns.go b/pkg/pipelineascode/cancel_pipelineruns.go index 1f420c3ac..4052936d4 100644 --- a/pkg/pipelineascode/cancel_pipelineruns.go +++ b/pkg/pipelineascode/cancel_pipelineruns.go @@ -24,18 +24,19 @@ var cancelMergePatch = map[string]interface{}{ } func (p *PacRun) cancelPipelineRuns(ctx context.Context, repo *v1alpha1.Repository) error { - if p.event.TriggerTarget != "pull_request" { - msg := fmt.Sprintf("not a pullRequest event, event: %v", p.event.TriggerTarget) - p.eventEmitter.EmitMessage(repo, zap.WarnLevel, "RepositoryEvent", msg) - return nil + labelSelector := getLabelSelector(map[string]string{ + keys.URLRepository: formatting.CleanValueKubernetes(p.event.Repository), + keys.SHA: formatting.CleanValueKubernetes(p.event.SHA), + }) + + if p.event.TriggerTarget == "pull_request" { + labelSelector = getLabelSelector(map[string]string{ + keys.PullRequest: strconv.Itoa(p.event.PullRequestNumber), + }) } prs, err := p.run.Clients.Tekton.TektonV1().PipelineRuns(repo.Namespace).List(ctx, metav1.ListOptions{ - LabelSelector: getLabelSelector(map[string]string{ - keys.URLRepository: formatting.CleanValueKubernetes(p.event.Repository), - keys.SHA: formatting.CleanValueKubernetes(p.event.SHA), - keys.PullRequest: strconv.Itoa(p.event.PullRequestNumber), - }), + LabelSelector: labelSelector, }) if err != nil { return fmt.Errorf("failed to list pipelineRuns : %w", err) diff --git a/pkg/provider/github/detect.go b/pkg/provider/github/detect.go index cd37ab201..50d0fdb7c 100644 --- a/pkg/provider/github/detect.go +++ b/pkg/provider/github/detect.go @@ -88,6 +88,19 @@ func detectTriggerTypeFromPayload(ghEventType string, eventInt any) (info.Trigge return info.TriggerTypeCheckRunRerequested, "" } return "", fmt.Sprintf("check_run: unsupported action \"%s\"", event.GetAction()) + case *github.CommitCommentEvent: + if event.GetAction() == "created" { + if provider.IsTestRetestComment(event.GetComment().GetBody()) { + return info.TriggerTypeRetest, "" + } + if provider.IsOkToTestComment(event.GetComment().GetBody()) { + return info.TriggerTypeOkToTest, "" + } + if provider.IsCancelComment(event.GetComment().GetBody()) { + return info.TriggerTypeCancel, "" + } + } + return "", fmt.Sprintf("commit_comment: unsupported action \"%s\"", event.GetAction()) } return "", fmt.Sprintf("github: event \"%v\" is not supported", ghEventType) } diff --git a/pkg/provider/github/detect_test.go b/pkg/provider/github/detect_test.go index 43721cdfd..311b4ee82 100644 --- a/pkg/provider/github/detect_test.go +++ b/pkg/provider/github/detect_test.go @@ -63,8 +63,17 @@ func TestProvider_Detect(t *testing.T) { event: github.CommitCommentEvent{ Action: github.String("something"), }, + eventType: "release", + wantReason: "event \"release\" is not supported", + isGH: true, + processReq: false, + }, + { + name: "invalid commit_comment Event", + event: github.CommitCommentEvent{ + Action: github.String("something"), + }, eventType: "commit_comment", - wantReason: "event \"commit_comment\" is not supported", isGH: true, processReq: false, }, @@ -228,7 +237,7 @@ func TestProvider_Detect(t *testing.T) { processReq: true, }, { - name: "issue comment Event with retest", + name: "issue comment Event with cancel comment ", event: github.IssueCommentEvent{ Action: github.String("created"), Issue: &github.Issue{ @@ -246,6 +255,45 @@ func TestProvider_Detect(t *testing.T) { isGH: true, processReq: true, }, + { + name: "commit comment event with cancel comment", + event: github.CommitCommentEvent{ + Action: github.String("created"), + Installation: &github.Installation{ + ID: github.Int64(123), + }, + Comment: &github.RepositoryComment{Body: github.String("/cancel")}, + }, + eventType: "commit_comment", + isGH: true, + processReq: true, + }, + { + name: "commit comment Event with retest", + event: github.CommitCommentEvent{ + Action: github.String("created"), + Installation: &github.Installation{ + ID: github.Int64(123), + }, + Comment: &github.RepositoryComment{Body: github.String("/retest")}, + }, + eventType: "commit_comment", + isGH: true, + processReq: true, + }, + { + name: "commit comment Event with test", + event: github.CommitCommentEvent{ + Action: github.String("created"), + Installation: &github.Installation{ + ID: github.Int64(123), + }, + Comment: &github.RepositoryComment{Body: github.String("/test")}, + }, + eventType: "commit_comment", + isGH: true, + processReq: true, + }, } for _, tt := range tests { diff --git a/pkg/provider/github/github.go b/pkg/provider/github/github.go index 1e5100f98..a57763845 100644 --- a/pkg/provider/github/github.go +++ b/pkg/provider/github/github.go @@ -544,3 +544,20 @@ func uniqueRepositoryID(repoIDs []int64, id int64) []int64 { } return r } + +// isBranchContainsCommit checks whether provided branch has sha or not. +func (v *Provider) isBranchContainsCommit(ctx context.Context, runevent *info.Event, branchName string) error { + if v.Client == nil { + return fmt.Errorf("no github client has been initialized, " + + "exiting... (hint: did you forget setting a secret on your repo?)") + } + + branchInfo, _, err := v.Client.Repositories.GetBranch(ctx, runevent.Organization, runevent.Repository, branchName, true) + if err != nil { + return err + } + if branchInfo.Commit.GetSHA() == runevent.SHA { + return nil + } + return fmt.Errorf("provided branch %s does not contains sha %s", branchName, runevent.SHA) +} diff --git a/pkg/provider/github/github_test.go b/pkg/provider/github/github_test.go index 240e7e3a4..42b6eabc4 100644 --- a/pkg/provider/github/github_test.go +++ b/pkg/provider/github/github_test.go @@ -998,3 +998,47 @@ func TestCreateToken(t *testing.T) { assert.Equal(t, strings.Contains(err.Error(), "could not refresh installation id 1234567's token"), true) } } + +func TestGetBranch(t *testing.T) { + tests := []struct { + name string + sha string + branchName string + wantErr bool + }{{ + name: "sha exist in the branch", + sha: "SHA1", + wantErr: false, + }, { + name: "sha doesn't exist in the branch", + sha: "SHA2", + wantErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runEvent := &info.Event{ + Organization: "pushrequestowner", + Repository: "pushrequestrepository", + SHA: tt.sha, + InstallationID: int64(1234567), + } + fakeclient, mux, _, teardown := ghtesthelper.SetupGH() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/branches/test1", + runEvent.Organization, runEvent.Repository), func(rw http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(rw, `{ + "name": "test1", + "commit": { + "sha": "SHA1" + }}}`) + assert.NilError(t, err) + }) + + ctx, _ := rtesting.SetupFakeContext(t) + provider := &Provider{Client: fakeclient} + err := provider.isBranchContainsCommit(ctx, runEvent, "test1") + assert.Equal(t, err != nil, tt.wantErr) + }) + } +} diff --git a/pkg/provider/github/parse_payload.go b/pkg/provider/github/parse_payload.go index 8c03c6a81..e215d7ecd 100644 --- a/pkg/provider/github/parse_payload.go +++ b/pkg/provider/github/parse_payload.go @@ -230,6 +230,11 @@ func (v *Provider) processEvent(ctx context.Context, event *info.Event, eventInt if err != nil { return nil, err } + case *github.CommitCommentEvent: + if v.Client == nil { + return nil, fmt.Errorf("gitops style comments operation is only supported with github apps integration") + } + return v.handleCommitCommentEvent(ctx, gitEvent) case *github.PushEvent: processedEvent.Organization = gitEvent.GetRepo().GetOwner().GetLogin() processedEvent.Repository = gitEvent.GetRepo().GetName() @@ -371,3 +376,62 @@ func (v *Provider) handleIssueCommentEvent(ctx context.Context, event *github.Is v.Logger.Infof("issue_comment: pipelinerun %s on %s/%s#%d has been requested", action, runevent.Organization, runevent.Repository, runevent.PullRequestNumber) return v.getPullRequest(ctx, runevent) } + +func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.CommitCommentEvent) (*info.Event, error) { + action := "push" + runevent := info.NewEvent() + runevent.Organization = event.GetRepo().GetOwner().GetLogin() + runevent.Repository = event.GetRepo().GetName() + runevent.Sender = event.GetSender().GetLogin() + runevent.URL = event.GetRepo().GetHTMLURL() + runevent.SHA = event.GetComment().GetCommitID() + runevent.HeadURL = runevent.URL + runevent.BaseURL = runevent.HeadURL + runevent.EventType = "push" + runevent.TriggerTarget = "push" + + // by default head and base branch is main + runevent.HeadBranch = "main" + runevent.BaseBranch = "main" + + var ( + branchName string + prName string + err error + ) + + // if it is a /test or /retest comment with pipelinerun name figure out the pipelinerun name + if provider.IsTestRetestComment(event.GetComment().GetBody()) { + prName, branchName, err = provider.GetPipelineRunAndBranchNameFromTestComment(event.GetComment().GetBody()) + if err != nil { + return runevent, err + } + runevent.TargetTestPipelineRun = prName + } + if provider.IsCancelComment(event.GetComment().GetBody()) { + action = "cancellation" + prName, branchName, err = provider.GetPipelineRunAndBranchNameFromCancelComment(event.GetComment().GetBody()) + if err != nil { + return runevent, err + } + runevent.TargetCancelPipelineRun = prName + } + + if branchName != "" { + if err = v.isBranchContainsCommit(ctx, runevent, branchName); err != nil { + return runevent, err + } + runevent.HeadBranch = branchName + runevent.BaseBranch = branchName + } + + if provider.IsCancelComment(event.GetComment().GetBody()) { + if err = v.isBranchContainsCommit(ctx, runevent, runevent.HeadBranch); err != nil { + runevent.CancelPipelineRuns = false + return runevent, err + } + runevent.CancelPipelineRuns = true + } + v.Logger.Infof("commit_comment: pipelinerun %s on %s/%s#%s has been requested", action, runevent.Organization, runevent.Repository, runevent.SHA) + return runevent, nil +} diff --git a/pkg/provider/github/parse_payload_test.go b/pkg/provider/github/parse_payload_test.go index eafe4c5fc..fe5c110d3 100644 --- a/pkg/provider/github/parse_payload_test.go +++ b/pkg/provider/github/parse_payload_test.go @@ -79,17 +79,19 @@ var samplePR = github.PullRequest{ func TestParsePayLoad(t *testing.T) { tests := []struct { - name string - wantErrString string - eventType string - payloadEventStruct interface{} - jeez string - triggerTarget string - githubClient bool - muxReplies map[string]interface{} - shaRet string - targetPipelinerun string - targetCancelPipelinerun string + name string + wantErrString string + eventType string + payloadEventStruct interface{} + jeez string + triggerTarget string + githubClient bool + muxReplies map[string]interface{} + shaRet string + targetPipelinerun string + targetCancelPipelinerun string + wantedBranchName string + isCancelPipelineRunEnabled bool }{ { name: "bad/unknown event", @@ -317,6 +319,161 @@ func TestParsePayLoad(t *testing.T) { shaRet: "samplePRsha", targetCancelPipelinerun: "dummy", }, + { + name: "bad/commit comment retest only with github apps", + wantErrString: "only supported with github apps", + eventType: "commit_comment", + triggerTarget: "push", + payloadEventStruct: github.CommitCommentEvent{Action: github.String("created")}, + }, + { + name: "good/commit comment for retest a pr", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/777"), + Body: github.String("/retest dummy"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/777": samplePR}, + shaRet: "samplePRsha", + targetPipelinerun: "dummy", + wantedBranchName: "main", + }, + { + name: "good/commit comment for retest all", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/777"), + Body: github.String("/retest"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/777": samplePR}, + shaRet: "samplePRsha", + wantedBranchName: "main", + }, + { + name: "good/commit comment for cancel all", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/999"), + Body: github.String("/cancel"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/999": samplePR}, + shaRet: "samplePRsha", + wantedBranchName: "main", + isCancelPipelineRunEnabled: true, + }, + { + name: "good/commit comment for cancel a pr", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/888"), + Body: github.String("/cancel dummy"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/888": samplePR}, + shaRet: "samplePRsha", + targetCancelPipelinerun: "dummy", + wantedBranchName: "main", + isCancelPipelineRunEnabled: true, + }, + { + name: "good/commit comment for retest with branch name", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/777"), + Body: github.String("/retest dummy branch:test1"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/7771": samplePR}, + shaRet: "samplePRsha", + targetPipelinerun: "dummy", + wantedBranchName: "test1", + isCancelPipelineRunEnabled: false, + }, + { + name: "good/commit comment for cancel all with branch name", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/999"), + Body: github.String("/cancel branch:test1"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/9991": samplePR}, + shaRet: "samplePRsha", + wantedBranchName: "test1", + isCancelPipelineRunEnabled: true, + }, + { + name: "good/commit comment for cancel a pr with branch name", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/888"), + Body: github.String("/cancel dummy branch:test1"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/8881": samplePR}, + shaRet: "samplePRsha", + targetCancelPipelinerun: "dummy", + wantedBranchName: "test1", + isCancelPipelineRunEnabled: true, + }, + { + name: "good/commit comment for cancel a pr with invalid branch name", + eventType: "commit_comment", + triggerTarget: "push", + githubClient: true, + payloadEventStruct: github.CommitCommentEvent{ + Repo: sampleRepo, + Comment: &github.RepositoryComment{ + CommitID: github.String("samplePRsha"), + HTMLURL: github.String("/888"), + Body: github.String("/cancel dummy branch:test2"), + }, + }, + muxReplies: map[string]interface{}{"/repos/owner/reponame/pulls/8881": samplePR}, + shaRet: "samplePRsha", + targetCancelPipelinerun: "dummy", + wantedBranchName: "test2", + isCancelPipelineRunEnabled: false, + wantErrString: "404 Not Found", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -333,6 +490,28 @@ func TestParsePayLoad(t *testing.T) { fmt.Fprint(rw, string(bjeez)) }) } + if tt.eventType == "commit_comment" { + mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/branches/test1", + "owner", "reponame"), func(rw http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(rw, `{ + "name": "test1", + "commit": { + "sha": "samplePRsha" + } + }`) + assert.NilError(t, err) + }) + mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/branches/main", + "owner", "reponame"), func(rw http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(rw, `{ + "name": "main", + "commit": { + "sha": "samplePRsha" + } + }`) + assert.NilError(t, err) + }) + } logger, _ := logger.GetLogger() gprovider := Provider{ Client: ghClient, @@ -361,6 +540,11 @@ func TestParsePayLoad(t *testing.T) { assert.NilError(t, err) assert.Assert(t, ret != nil) assert.Equal(t, tt.shaRet, ret.SHA) + if tt.eventType == "commit_comment" { + assert.Equal(t, tt.wantedBranchName, ret.HeadBranch) + assert.Equal(t, tt.wantedBranchName, ret.BaseBranch) + assert.Equal(t, tt.isCancelPipelineRunEnabled, ret.CancelPipelineRuns) + } if tt.targetPipelinerun != "" { assert.Equal(t, tt.targetPipelinerun, ret.TargetTestPipelineRun) } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 811620e71..767912f5d 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -1,6 +1,7 @@ package provider import ( + "fmt" "net/url" "regexp" "strings" @@ -64,6 +65,51 @@ func getNameFromComment(typeOfComment, comment string) string { return strings.TrimSpace(getFirstLine[0]) } +func GetPipelineRunAndBranchNameFromTestComment(comment string) (string, string, error) { + if strings.Contains(comment, testComment) { + return getPipelineRunAndBranchNameFromComment(testComment, comment) + } + return getPipelineRunAndBranchNameFromComment(retestComment, comment) +} + +func GetPipelineRunAndBranchNameFromCancelComment(comment string) (string, string, error) { + return getPipelineRunAndBranchNameFromComment(cancelComment, comment) +} + +// getPipelineRunAndBranchNameFromComment function will take GitOps comment and split the comment +// by /test, /retest or /cancel to return branch name and pipelinerun name. +func getPipelineRunAndBranchNameFromComment(typeOfComment, comment string) (string, string, error) { + var prName, branchName string + splitTest := strings.Split(comment, typeOfComment) + + // after the split get the second part of the typeOfComment (/test, /retest or /cancel) + // as second part can be branch name or pipelinerun name and branch name + // ex: /test branch:nightly, /test prname branch:nightly + if splitTest[1] != "" && strings.Contains(splitTest[1], ":") { + branchData := strings.Split(splitTest[1], ":") + + // make sure no other word is supported other than branch word + if !strings.Contains(branchData[0], "branch") { + return prName, branchName, fmt.Errorf("the GitOps comment%s does not contain a branch word", branchData[0]) + } + branchName = strings.Split(strings.TrimSpace(branchData[1]), " ")[0] + + // if data after the split contains prname then fetch that + prData := strings.Split(strings.TrimSpace(branchData[0]), " ") + if len(prData) > 1 { + prName = strings.TrimSpace(prData[0]) + } + } else { + // get the second part of the typeOfComment (/test, /retest or /cancel) + // as second part contains pipelinerun name + // ex: /test prname + getFirstLine := strings.Split(splitTest[1], "\n") + // trim spaces + prName = strings.TrimSpace(getFirstLine[0]) + } + return prName, branchName, nil +} + // CompareHostOfURLS compares the host of two parsed URLs and returns true if // they are func CompareHostOfURLS(uri1, uri2 string) bool { diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go index c2c27c193..423fe7d37 100644 --- a/pkg/provider/provider_test.go +++ b/pkg/provider/provider_test.go @@ -281,6 +281,182 @@ func TestGetPipelineRunFromCancelComment(t *testing.T) { } } +func TestGetPipelineRunAndBranchNameFromTestComment(t *testing.T) { + tests := []struct { + name string + comment string + branchName string + prName string + wantError bool + }{ + { + name: "retest all on test branch", + comment: "/retest branch:test", + branchName: "test", + wantError: false, + }, + { + name: "test a pipeline on test branch", + comment: "/test abc-01-pr branch:test", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for test command before branch name test", + comment: "/test abc-01-pr abc \n branch:test", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for retest command after branch name test", + comment: "/retest abc-01-pr branch:test \n abc", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for test command before and after branch name test", + comment: "/test abc-01-pr \n before branch:test \n after", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "different word other than branch for retest command", + comment: "/retest invalidname:nightly", + wantError: true, + }, + { + name: "test all", + comment: "/test", + wantError: false, + }, + { + name: "test a pipeline", + comment: "/test abc-01-pr", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "string before retest command", + comment: "abc \n /retest abc-01-pr", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "string after retest command", + comment: "/retest abc-01-pr \n abc", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "string before and after test command", + comment: "before \n /test abc-01-pr \n after", + prName: "abc-01-pr", + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prName, branchName, err := GetPipelineRunAndBranchNameFromTestComment(tt.comment) + assert.Equal(t, tt.wantError, err != nil) + assert.Equal(t, tt.branchName, branchName) + assert.Equal(t, tt.prName, prName) + }) + } +} + +func TestGetPipelineRunAndBranchNameFromCancelComment(t *testing.T) { + tests := []struct { + name string + comment string + branchName string + prName string + wantError bool + }{ + { + name: "cancel all pipeline", + comment: "/cancel", + wantError: false, + }, + { + name: "cancel a particular pipeline", + comment: "/cancel abc-01-pr", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "add string before cancel command", + comment: "abc \n /cancel abc-01-pr", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "add string after cancel command", + comment: "/cancel abc-01-pr \n abc", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "add string before and after cancel command", + comment: "before \n /cancel abc-01-pr \n after", + prName: "abc-01-pr", + wantError: false, + }, + { + name: "cancel all on test branch", + comment: "/cancel branch:test", + branchName: "test", + wantError: false, + }, + { + name: "cancel a pipeline on test branch", + comment: "/cancel abc-01-pr branch:test", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for cancel command before branch name test", + comment: "/cancel abc-01-pr abc \n branch:test", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for cancel command after branch name test", + comment: "/cancel abc-01-pr branch:test \n abc", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "string for cancel command before and after branch name test", + comment: "/cancel abc-01-pr \n before branch:test \n after", + prName: "abc-01-pr", + branchName: "test", + wantError: false, + }, + { + name: "different word other than branch for cancel command", + comment: "/cancel invalidname:nightly", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prName, branchName, err := GetPipelineRunAndBranchNameFromCancelComment(tt.comment) + assert.Equal(t, tt.wantError, err != nil) + assert.Equal(t, tt.branchName, branchName) + assert.Equal(t, tt.prName, prName) + }) + } +} + func TestCompareHostOfURLS(t *testing.T) { tests := []struct { name string diff --git a/test/github_push_retest_test.go b/test/github_push_retest_test.go new file mode 100644 index 000000000..8ce5c5653 --- /dev/null +++ b/test/github_push_retest_test.go @@ -0,0 +1,96 @@ +//go:build e2e +// +build e2e + +package test + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-github/v55/github" + "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys" + tgithub "github.com/openshift-pipelines/pipelines-as-code/test/pkg/github" + twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait" + "gotest.tools/v3/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGithubPushRequestGitOpsComments(t *testing.T) { + ctx := context.Background() + runcnx, ghcnx, opts, targetNS, targetRefName, prNumber, sha := tgithub.RunPushRequest(ctx, t, + "Github Push Request", []string{"testdata/pipelinerun-on-push.yaml", "testdata/pipelinerun.yaml"}, false) + defer tgithub.TearDown(ctx, t, runcnx, ghcnx, prNumber, targetRefName, targetNS, opts) + + pruns, err := runcnx.Clients.Tekton.TektonV1().PipelineRuns(targetNS).List(ctx, metav1.ListOptions{}) + assert.NilError(t, err) + assert.Assert(t, len(pruns.Items) == 2) + + tests := []struct { + name, comment string + prNum int + }{ + { + name: "Retest", + comment: "/retest branch:" + targetNS, + prNum: 4, + }, + { + name: "Test and Cancel PipelineRun", + comment: "/cancel pipelinerun-on-push branch:" + targetNS, + prNum: 5, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + waitOpts := twait.Opts{ + RepoName: targetNS, + Namespace: targetNS, + MinNumberStatus: tt.prNum, + PollTimeout: twait.DefaultTimeout, + TargetSHA: sha, + } + if tt.comment == "/cancel pipelinerun-on-push branch:"+targetNS { + runcnx.Clients.Log.Info("/test pipelinerun-on-push on Push Request before canceling") + _, _, err = ghcnx.Client.Repositories.CreateComment(ctx, + opts.Organization, + opts.Repo, sha, + &github.RepositoryComment{Body: github.String("/test pipelinerun-on-push branch:" + targetNS)}) + assert.NilError(t, err) + for { + prs, err := runcnx.Clients.Tekton.TektonV1().PipelineRuns(waitOpts.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", keys.SHA, waitOpts.TargetSHA), + }) + assert.NilError(t, err) + if len(prs.Items) == tt.prNum { + break + } + } + } + runcnx.Clients.Log.Infof("%s on Push Request", tt.comment) + _, _, err = ghcnx.Client.Repositories.CreateComment(ctx, + opts.Organization, + opts.Repo, sha, + &github.RepositoryComment{Body: github.String(tt.comment)}) + assert.NilError(t, err) + + runcnx.Clients.Log.Info("Waiting for Repository to be updated") + err = twait.UntilRepositoryUpdated(ctx, runcnx.Clients, waitOpts) + assert.NilError(t, err) + + runcnx.Clients.Log.Infof("Check if we have the repository set as succeeded") + repo, err := runcnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(targetNS).Get(ctx, targetNS, metav1.GetOptions{}) + assert.NilError(t, err) + if tt.comment == "/cancel pipelinerun-on-push branch:"+targetNS { + assert.Assert(t, repo.Status[len(repo.Status)-1].Conditions[0].Status == corev1.ConditionFalse) + } else { + assert.Assert(t, repo.Status[len(repo.Status)-1].Conditions[0].Status == corev1.ConditionTrue) + } + + pruns, err = runcnx.Clients.Tekton.TektonV1().PipelineRuns(targetNS).List(ctx, metav1.ListOptions{}) + assert.NilError(t, err) + assert.Assert(t, len(pruns.Items) == tt.prNum) + }) + } +} diff --git a/test/github_push_test.go b/test/github_push_test.go index e4d17aedf..b58a7ba7b 100644 --- a/test/github_push_test.go +++ b/test/github_push_test.go @@ -5,81 +5,24 @@ package test import ( "context" - "fmt" - "net/http" "os" "testing" tgithub "github.com/openshift-pipelines/pipelines-as-code/test/pkg/github" - "github.com/openshift-pipelines/pipelines-as-code/test/pkg/payload" - twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait" - "github.com/tektoncd/pipeline/pkg/names" - "gotest.tools/v3/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestGithubPush(t *testing.T) { if os.Getenv("NIGHTLY_E2E_TEST") != "true" { t.Skip("Skipping test since only enabled for nightly") } + ctx := context.Background() for _, onWebhook := range []bool{false, true} { - targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-push") - targetBranch := targetNS - targetEvent := "push" if onWebhook && os.Getenv("TEST_GITHUB_REPO_OWNER_WEBHOOK") == "" { t.Skip("TEST_GITHUB_REPO_OWNER_WEBHOOK is not set") continue } - - ctx := context.Background() - runcnx, opts, gprovider, err := tgithub.Setup(ctx, onWebhook) - assert.NilError(t, err) - - if onWebhook { - runcnx.Clients.Log.Info("Testing with Direct Webhook integration") - } else { - runcnx.Clients.Log.Info("Testing with Github APPS integration") - } - repoinfo, resp, err := gprovider.Client.Repositories.Get(ctx, opts.Organization, opts.Repo) - assert.NilError(t, err) - if resp != nil && resp.StatusCode == http.StatusNotFound { - t.Errorf("Repository %s not found in %s", opts.Organization, opts.Repo) - } - err = tgithub.CreateCRD(ctx, t, repoinfo, runcnx, opts, targetNS) - assert.NilError(t, err) - - entries, err := payload.GetEntries( - map[string]string{".tekton/pipelinerun-on-push.yaml": "testdata/pipelinerun-on-push.yaml"}, - targetNS, targetBranch, targetEvent, map[string]string{}) - assert.NilError(t, err) - - title := "TestPush " - if onWebhook { - title += "OnWebhook" - } - title += "- " + targetBranch - - targetRefName := fmt.Sprintf("refs/heads/%s", targetBranch) - sha, err := tgithub.PushFilesToRef(ctx, gprovider.Client, title, repoinfo.GetDefaultBranch(), targetRefName, opts.Organization, opts.Repo, entries) - runcnx.Clients.Log.Infof("Commit %s has been created and pushed to %s", sha, targetRefName) - assert.NilError(t, err) - defer tgithub.TearDown(ctx, t, runcnx, gprovider, -1, targetRefName, targetNS, opts) - - runcnx.Clients.Log.Infof("Waiting for Repository to be updated") - waitOpts := twait.Opts{ - RepoName: targetNS, - Namespace: targetNS, - MinNumberStatus: 1, - PollTimeout: twait.DefaultTimeout, - TargetSHA: sha, - } - err = twait.UntilRepositoryUpdated(ctx, runcnx.Clients, waitOpts) - assert.NilError(t, err) - - runcnx.Clients.Log.Infof("Check if we have the repository set as succeeded") - repo, err := runcnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(targetNS).Get(ctx, targetNS, metav1.GetOptions{}) - assert.NilError(t, err) - assert.Assert(t, repo.Status[len(repo.Status)-1].Conditions[0].Status == corev1.ConditionTrue) + runcnx, ghcnx, opts, targetNS, targetRefName, prNumber, _ := tgithub.RunPushRequest(ctx, t, + "Github Push Request", []string{"testdata/pipelinerun-on-push.yaml"}, onWebhook) + defer tgithub.TearDown(ctx, t, runcnx, ghcnx, prNumber, targetRefName, targetNS, opts) } } diff --git a/test/pkg/github/pr.go b/test/pkg/github/pr.go index 416d6dc2f..a504ee17f 100644 --- a/test/pkg/github/pr.go +++ b/test/pkg/github/pr.go @@ -147,3 +147,51 @@ func RunPullRequest(ctx context.Context, t *testing.T, label string, yamlFiles [ wait.Succeeded(ctx, t, runcnx, opts, sopt) return runcnx, ghcnx, opts, targetNS, targetRefName, number, sha } + +func RunPushRequest(ctx context.Context, t *testing.T, label string, yamlFiles []string, onWebhook bool) (*params.Run, *ghprovider.Provider, options.E2E, string, string, int, string) { + targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-push") + targetBranch := targetNS + targetEvent := "push" + runcnx, opts, ghcnx, err := Setup(ctx, onWebhook) + assert.NilError(t, err) + + var logmsg string + if onWebhook { + logmsg = fmt.Sprintf("Testing %s with Direct Webhook integration on %s", label, targetNS) + runcnx.Clients.Log.Info(logmsg) + } else { + logmsg = fmt.Sprintf("Testing %s with Github APPS integration on %s", label, targetNS) + runcnx.Clients.Log.Info(logmsg) + } + repoinfo, resp, err := ghcnx.Client.Repositories.Get(ctx, opts.Organization, opts.Repo) + assert.NilError(t, err) + if resp != nil && resp.StatusCode == http.StatusNotFound { + t.Errorf("Repository %s not found in %s", opts.Organization, opts.Repo) + } + err = CreateCRD(ctx, t, repoinfo, runcnx, opts, targetNS) + assert.NilError(t, err) + + yamlEntries := map[string]string{} + for _, v := range yamlFiles { + yamlEntries[filepath.Join(".tekton", filepath.Base(v))] = v + } + + entries, err := payload.GetEntries(yamlEntries, + targetNS, targetBranch, targetEvent, map[string]string{}) + assert.NilError(t, err) + + targetRefName := fmt.Sprintf("refs/heads/%s", targetBranch) + sha, err := PushFilesToRef(ctx, ghcnx.Client, logmsg, repoinfo.GetDefaultBranch(), targetRefName, opts.Organization, opts.Repo, entries) + runcnx.Clients.Log.Infof("Commit %s has been created and pushed to %s", sha, targetRefName) + assert.NilError(t, err) + + sopt := wait.SuccessOpt{ + Title: logmsg, + OnEvent: options.PushEvent, + TargetNS: targetNS, + NumberofPRMatch: len(yamlFiles), + SHA: sha, + } + wait.Succeeded(ctx, t, runcnx, opts, sopt) + return runcnx, ghcnx, opts, targetNS, targetRefName, -1, sha +} diff --git a/test/testdata/pipelinerun-on-push.yaml b/test/testdata/pipelinerun-on-push.yaml index 1e1d11778..a2d8bd977 100644 --- a/test/testdata/pipelinerun-on-push.yaml +++ b/test/testdata/pipelinerun-on-push.yaml @@ -2,7 +2,7 @@ apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: - name: "\\ .PipelineName //" + name: "pipelinerun-on-push" annotations: pipelinesascode.tekton.dev/target-namespace: "\\ .TargetNamespace //" pipelinesascode.tekton.dev/on-target-branch: "[\\ .TargetBranch //]"