diff --git a/docs/content/docs/guide/statuses.md b/docs/content/docs/guide/statuses.md index 39a88ab2d..2ae6e3110 100644 --- a/docs/content/docs/guide/statuses.md +++ b/docs/content/docs/guide/statuses.md @@ -137,3 +137,17 @@ generating artifacts on every push. The relevant section of the pipeline can be found here: + +## Tekton Results Annotation + +The specialized annotation for Tekton Results is stored in a specific format: + +```json +results.tekton.dev/recordSummaryAnnotations:{"repo":"pac-demo","commit":"62f8c8b7e4c3fc38cfbe7fcce2660e5b95de2d9a","eventType":"pull_request","pull_request-id":7} +``` + +This annotation is automatically added by Pipelines-as-Code to every PipelineRun +it initiates. +Unlike other annotations, which might be stored in `PipelineRun` or `TaskRun` metadata, +this custom annotation is selectively captured to provide additional information +specifically in the Tekton Results API. diff --git a/pkg/kubeinteraction/labels.go b/pkg/kubeinteraction/labels.go index 56bb371a5..eecb54cc0 100644 --- a/pkg/kubeinteraction/labels.go +++ b/pkg/kubeinteraction/labels.go @@ -1,6 +1,7 @@ package kubeinteraction import ( + "fmt" "strconv" "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode" @@ -19,7 +20,7 @@ const ( StateFailed = "failed" ) -func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRun, repo *apipac.Repository, providerinfo *info.ProviderConfig) { +func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRun, repo *apipac.Repository, providerinfo *info.ProviderConfig) error { // Add labels on the soon to be created pipelinerun so UI/CLI can easily // query them. labels := map[string]string{ @@ -85,4 +86,12 @@ func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRu for k, v := range annotations { pipelineRun.Annotations[k] = v } + + // Add annotations to PipelineRuns to integrate with Tekton Results + err := AddResultsAnnotation(event, pipelineRun) + if err != nil { + return fmt.Errorf("failed to add results annotations with error: %w", err) + } + + return nil } diff --git a/pkg/kubeinteraction/labels_test.go b/pkg/kubeinteraction/labels_test.go index 08f41640d..ff2d27f5d 100644 --- a/pkg/kubeinteraction/labels_test.go +++ b/pkg/kubeinteraction/labels_test.go @@ -50,7 +50,8 @@ func TestAddLabelsAndAnnotations(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AddLabelsAndAnnotations(tt.args.event, tt.args.pipelineRun, tt.args.repo, &info.ProviderConfig{}) + err := AddLabelsAndAnnotations(tt.args.event, tt.args.pipelineRun, tt.args.repo, &info.ProviderConfig{}) + assert.NilError(t, err) assert.Assert(t, tt.args.pipelineRun.Labels[keys.URLOrg] == tt.args.event.Organization, "'%s' != %s", tt.args.pipelineRun.Labels[keys.URLOrg], tt.args.event.Organization) assert.Assert(t, tt.args.pipelineRun.Annotations[keys.URLOrg] == tt.args.event.Organization, "'%s' != %s", diff --git a/pkg/kubeinteraction/resultsannotation.go b/pkg/kubeinteraction/resultsannotation.go new file mode 100644 index 000000000..3a9e34e43 --- /dev/null +++ b/pkg/kubeinteraction/resultsannotation.go @@ -0,0 +1,45 @@ +package kubeinteraction + +import ( + "encoding/json" + "fmt" + + "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" +) + +const ( + resGroupName = "results.tekton.dev" + recordSummaryAnnotation = "/recordSummaryAnnotations" +) + +type ResultAnnotation struct { + Repo string `json:"repo"` + Commit string `json:"commit"` + EventType string `json:"eventType"` + PullRequestID int `json:"pull_request-id,omitempty"` +} + +// Add annotation to PipelineRuns produced by PaC for TektonResults +// to capture data for summary and record. +func AddResultsAnnotation(event *info.Event, pipelineRun *tektonv1.PipelineRun) error { + if event == nil { + return fmt.Errorf("nil event") + } + resultAnnotation := ResultAnnotation{ + Repo: event.Repository, + Commit: event.SHA, + EventType: event.EventType, + PullRequestID: event.PullRequestNumber, + } + + // convert the `resultAnnotation` sturct into JSON string + resAnnotationJSON, err := json.Marshal(resultAnnotation) + if err != nil { + return err + } + // append the result annotation + pipelineRun.Annotations[resGroupName+recordSummaryAnnotation] = string(resAnnotationJSON) + + return nil +} diff --git a/pkg/kubeinteraction/resultsannotation_test.go b/pkg/kubeinteraction/resultsannotation_test.go new file mode 100644 index 000000000..254b94b6e --- /dev/null +++ b/pkg/kubeinteraction/resultsannotation_test.go @@ -0,0 +1,77 @@ +package kubeinteraction + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "gotest.tools/v3/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAddResultsAnnotation(t *testing.T) { + testCases := []struct { + name string + event *info.Event + expectedError error + }{ + { + name: "Valid Event", + event: &info.Event{ + Repository: "tektoncd/results", + SHA: "8789abb6", + EventType: "PR", + PullRequestNumber: 123, + }, + expectedError: nil, + }, + { + name: "Empty Event", + event: &info.Event{}, + expectedError: nil, + }, + { + name: "Nil Event", + event: nil, + expectedError: errors.New("nil event"), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Prepare test data + pipelineRun := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + } + err := AddResultsAnnotation(tt.event, pipelineRun) + + // Check if the error matches the expected error + if !errors.Is(err, tt.expectedError) { + assert.Equal(t, err.Error(), "nil event") + } + + // If no error, check annotations + if err == nil { + // Expected result annotation + resultAnnotation := ResultAnnotation{ + Repo: tt.event.Repository, + Commit: tt.event.SHA, + EventType: tt.event.EventType, + PullRequestID: tt.event.PullRequestNumber, + } + expectedJSON, err := json.Marshal(resultAnnotation) + if err != nil { + t.Fatalf("Failed to marshal expected result annotation: %v", err) + } + expectedAnnotation := string(expectedJSON) + + // Check if annotation is added correctly + assert.Assert(t, pipelineRun.Annotations[resGroupName+recordSummaryAnnotation] == expectedAnnotation, "Unexpected record summary annotation. Expected: %s, Got: %s", expectedAnnotation, pipelineRun.Annotations[resGroupName+recordSummaryAnnotation]) + } + }) + } +} diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go index 4e21d59ac..2e0eb59b0 100644 --- a/pkg/pipelineascode/pipelineascode.go +++ b/pkg/pipelineascode/pipelineascode.go @@ -146,7 +146,10 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi } // Add labels and annotations to pipelinerun - kubeinteraction.AddLabelsAndAnnotations(p.event, match.PipelineRun, match.Repo, p.vcx.GetConfig()) + err := kubeinteraction.AddLabelsAndAnnotations(p.event, match.PipelineRun, match.Repo, p.vcx.GetConfig()) + if err != nil { + p.logger.Errorf("Error adding labels/annotations to PipelineRun '%s' in namespace '%s': %v", match.PipelineRun.GetName(), match.Repo.GetNamespace(), err) + } // if concurrency is defined then start the pipelineRun in pending state and // state as queued diff --git a/test/gitea_results_annotation_test.go b/test/gitea_results_annotation_test.go new file mode 100644 index 000000000..d0c175809 --- /dev/null +++ b/test/gitea_results_annotation_test.go @@ -0,0 +1,65 @@ +//go:build e2e +// +build e2e + +package test + +import ( + "context" + "encoding/json" + "strconv" + "testing" + + "github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction" + tgitea "github.com/openshift-pipelines/pipelines-as-code/test/pkg/gitea" + "github.com/openshift-pipelines/pipelines-as-code/test/pkg/options" + "gotest.tools/v3/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + AnnotationPullRequest = "pipelinesascode.tekton.dev/pull-request" + AnnotationSummary = "results.tekton.dev/recordSummaryAnnotations" +) + +func TestGiteaResultsAnnotations(t *testing.T) { + topts := &tgitea.TestOpts{ + Regexp: successRegexp, + TargetEvent: options.PullRequestEvent, + YAMLFiles: map[string]string{ + ".tekton/pipeline.yaml": "testdata/pipelinerun.yaml", + }, + CheckForStatus: "success", + } + defer tgitea.TestPR(t, topts)() + + // assertions for checking results specific annotation in the PipelineRuns manifest here + prs, err := topts.ParamsRun.Clients.Tekton.TektonV1().PipelineRuns(topts.TargetNS).List(context.Background(), metav1.ListOptions{}) + assert.NilError(t, err) + for _, pr := range prs.Items { + annotations := pr.GetAnnotations() + assert.Assert(t, annotations != nil, "Annotations should not be nil") + + val, exists := annotations[AnnotationPullRequest] + if !exists { + t.Fatalf("Annotation %s does not exist", AnnotationPullRequest) + } + + pullRequestNumber, err := strconv.Atoi(val) + assert.NilError(t, err) + + // Assert specific annotation + resultAnnotation := kubeinteraction.ResultAnnotation{ + Repo: topts.TargetNS, + Commit: topts.PullRequest.Head.Sha, + EventType: topts.TargetEvent, + PullRequestID: pullRequestNumber, + } + expectedJSON, err := json.Marshal(resultAnnotation) + assert.NilError(t, err) + expectedResultAnnotation := string(expectedJSON) + + // an example of results annotation format + // results.tekton.dev/recordSummaryAnnotations:{"repo":"pac-demo","commit":"62f8c8b7e4c3fc38cfbe7fcce2660e5b95de2d9a","eventType":"pull_request","pull_request-id":7} + assert.Equal(t, annotations[AnnotationSummary], expectedResultAnnotation, "Unexpected annotation value") + } +}