diff --git a/docs/content/docs/guide/repositorycrd.md b/docs/content/docs/guide/repositorycrd.md index f598cf6ad..5fe64ced2 100644 --- a/docs/content/docs/guide/repositorycrd.md +++ b/docs/content/docs/guide/repositorycrd.md @@ -169,6 +169,14 @@ of the pipelineruns will be executed in alphabetical order, one after the other. At any given time, only one pipeline run will be in the running state, while the rest will be queued. +### Kueue - Kubernetes-native Job Queueing + +Pipelines-as-Code now accommodates [Kueue](https://kueue.sigs.k8s.io/) as an alternative, Kubernetes-native solution for queuing PipelineRun. +To get started, you can deploy the experimental integration provided by the [konflux-ci/tekton-kueue](https://github.com/konflux-ci/tekton-kueue) project. This allows you to schedule PipelineRuns through Kueue's queuing mechanism. + +Note: The [konflux-ci/tekton-kueue](https://github.com/konflux-ci/tekton-kueue) project and the Pipelines-as-Code integration is only intended for testing +It is only meant for experimentation and should not be used in production environments. + ## Scoping GitHub token to a list of private and public repositories within and outside namespaces By default, the GitHub token that Pipelines-as-Code generates is scoped only to the repository where the payload comes from. diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go index 6baa732c8..b49c299f3 100644 --- a/pkg/pipelineascode/pipelineascode.go +++ b/pkg/pipelineascode/pipelineascode.go @@ -14,7 +14,6 @@ import ( "github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction" "github.com/openshift-pipelines/pipelines-as-code/pkg/matcher" "github.com/openshift-pipelines/pipelines-as-code/pkg/params" - "github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients" "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info" "github.com/openshift-pipelines/pipelines-as-code/pkg/params/settings" "github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype" @@ -241,12 +240,23 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi OriginalPipelineRunName: pr.GetAnnotations()[keys.OriginalPRName], } + patchAnnotations := map[string]string{} + patchLabels := map[string]string{} + whatPatching := "" // if pipelineRun is in pending state then report status as queued if pr.Spec.Status == tektonv1.PipelineRunSpecStatusPending { status.Status = queuedStatus if status.Text, err = mt.MakeTemplate(p.vcx.GetTemplate(provider.QueueingPipelineType)); err != nil { return nil, fmt.Errorf("cannot create message template: %w", err) } + // If the PipelineRun is in the "queued" state, add the appropriate label and annotation. + // These are later used by the watcher to determine whether the PipelineRun status + // should be reported back to the Git provider. We do add the `state` annotations and label when + // concurrency is enabled but this would happen when PipelineRun's status has been changed by + // the other controller and PaC is not aware of that change. + whatPatching = "annotations.state and labels.state" + patchAnnotations[keys.State] = kubeinteraction.StateQueued + patchLabels[keys.State] = kubeinteraction.StateQueued } if err := p.vcx.CreateStatus(ctx, p.event, status); err != nil { @@ -257,7 +267,12 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi // Patch pipelineRun with logURL annotation, skips for GitHub App as we patch logURL while patching CheckrunID if _, ok := pr.Annotations[keys.InstallationID]; !ok { - pr, err = action.PatchPipelineRun(ctx, p.logger, "logURL", p.run.Clients.Tekton, pr, getLogURLMergePatch(p.run.Clients, pr)) + patchAnnotations[keys.LogURL] = p.run.Clients.ConsoleUI().DetailURL(pr) + whatPatching = "annotations.logURL, " + whatPatching + } + + if len(patchAnnotations) > 0 || len(patchLabels) > 0 { + pr, err = action.PatchPipelineRun(ctx, p.logger, whatPatching, p.run.Clients.Tekton, pr, getMergePatch(patchAnnotations, patchLabels)) if err != nil { // we still return the created PR with error, and allow caller to decide what to do with the PR, and avoid // unneeded SIGSEGV's @@ -268,12 +283,11 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi return pr, nil } -func getLogURLMergePatch(clients clients.Clients, pr *tektonv1.PipelineRun) map[string]any { +func getMergePatch(annotations, labels map[string]string) map[string]any { return map[string]any{ "metadata": map[string]any{ - "annotations": map[string]string{ - keys.LogURL: clients.ConsoleUI().DetailURL(pr), - }, + "annotations": annotations, + "labels": labels, }, } } diff --git a/pkg/pipelineascode/pipelineascode_test.go b/pkg/pipelineascode/pipelineascode_test.go index 5be8479f8..9b9f23e5c 100644 --- a/pkg/pipelineascode/pipelineascode_test.go +++ b/pkg/pipelineascode/pipelineascode_test.go @@ -497,6 +497,49 @@ func TestRun(t *testing.T) { finalStatusText: "User fantasio is not allowed to trigger CI by GitOps comment on push commit in this repo.", skipReplyingOrgPublicMembers: true, }, + { + name: "pull request/pipelinerun created in pending state (state changed by other controller)", + runevent: info.Event{ + Event: &github.PullRequestEvent{ + PullRequest: &github.PullRequest{ + Number: github.Ptr(666), + }, + }, + SHA: "fromwebhook", + Organization: "owner", + Sender: "owner", + Repository: "repo", + URL: "https://service/documentation", + HeadBranch: "press", + BaseBranch: "main", + EventType: "pull_request", + TriggerTarget: "pull_request", + PullRequestNumber: 666, + InstallationID: 1234, + }, + tektondir: "testdata/pending_pipelinerun", + }, + { + name: "pull request/pipelinerun created in pending state without installationID (state changed by other controller)", + runevent: info.Event{ + Event: &github.PullRequestEvent{ + PullRequest: &github.PullRequest{ + Number: github.Ptr(666), + }, + }, + SHA: "fromwebhook", + Organization: "owner", + Sender: "owner", + Repository: "repo", + URL: "https://service/documentation", + HeadBranch: "press", + BaseBranch: "main", + EventType: "pull_request", + TriggerTarget: "pull_request", + PullRequestNumber: 666, + }, + tektondir: "testdata/pending_pipelinerun", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -675,10 +718,10 @@ func TestRun(t *testing.T) { secretName, ok := pr.GetAnnotations()[keys.GitAuthSecret] assert.Assert(t, ok, "Cannot find secret %s on annotations", secretName) } - if tt.concurrencyLimit > 0 { - concurrencyLimit, ok := pr.GetAnnotations()[keys.State] - assert.Assert(t, ok, "State hasn't been set on PR", concurrencyLimit) - assert.Equal(t, concurrencyLimit, kubeinteraction.StateQueued) + if tt.concurrencyLimit > 0 || pr.Spec.Status == pipelinev1.PipelineRunSpecStatusPending { + state, ok := pr.GetAnnotations()[keys.State] + assert.Assert(t, ok, "State hasn't been set on PR", state) + assert.Equal(t, state, kubeinteraction.StateQueued) } } } @@ -690,12 +733,8 @@ func TestGetLogURLMergePatch(t *testing.T) { con := consoleui.FallBackConsole{} clients := clients.Clients{} clients.SetConsoleUI(con) - pr := &pipelinev1.PipelineRun{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-pipeline-run", - }, - } - result := getLogURLMergePatch(clients, pr) + ann := map[string]string{keys.LogURL: con.URL()} + result := getMergePatch(ann, map[string]string{}) m, ok := result["metadata"].(map[string]any) assert.Assert(t, ok) a, ok := m["annotations"].(map[string]string) diff --git a/pkg/pipelineascode/testdata/pending_pipelinerun/.tekton/run.yaml b/pkg/pipelineascode/testdata/pending_pipelinerun/.tekton/run.yaml new file mode 100644 index 000000000..389fd2653 --- /dev/null +++ b/pkg/pipelineascode/testdata/pending_pipelinerun/.tekton/run.yaml @@ -0,0 +1,17 @@ +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: pull_request-test1 + annotations: + pipelinesascode.tekton.dev/on-target-branch: "[main]" + pipelinesascode.tekton.dev/on-event: "[pull_request]" +spec: + status: PipelineRunPending + pipelineSpec: + tasks: + - name: max + taskSpec: + steps: + - name: success + image: registry.access.redhat.com/ubi9/ubi-minimal + script: 'exit 0' diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index a7e4237b8..591e7b97b 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -60,6 +60,24 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, pr *tektonv1.PipelineRun return nil } + reason := "" + if len(pr.Status.GetConditions()) > 0 { + reason = pr.Status.GetConditions()[0].GetReason() + } + // This condition handles cases where the PipelineRun has entered a "Running" state, + // but its status in the Git provider remains "queued" (e.g., due to updates made by + // another controller outside PaC). To maintain consistency between the PipelineRun + // status and the Git provider status, we update both the PipelineRun resource and + // the corresponding status on the Git provider here. + if reason == string(tektonv1.PipelineRunReasonRunning) && state == kubeinteraction.StateQueued { + repoName := pr.GetAnnotations()[keys.Repository] + repo, err := r.repoLister.Repositories(pr.Namespace).Get(repoName) + if err != nil { + return fmt.Errorf("failed to get repository CR: %w", err) + } + return r.updatePipelineRunToInProgress(ctx, logger, repo, pr) + } + // if its a GitHub App pipelineRun PR then process only if check run id is added otherwise wait if _, ok := pr.Annotations[keys.InstallationID]; ok { if _, ok := pr.Annotations[keys.CheckRunID]; !ok {