diff --git a/config/300-repositories.yaml b/config/300-repositories.yaml
index 0e361d8d81..e74b522520 100644
--- a/config/300-repositories.yaml
+++ b/config/300-repositories.yaml
@@ -432,6 +432,36 @@ spec:
type: string
type: array
type: object
+ status_checks:
+ description: StatusChecks configures the status checks for the repository.
+ properties:
+ enabled:
+ description: Enabled defines if the status checks should be reported. Default is false.
+ type: boolean
+ mode:
+ description: |-
+ Mode defines how the status checks should be reported when is enabled.
+ Options:
+ - 'per_pipelinerun': Report the status check of each PipelineRun separately.
+ enum:
+ - ""
+ - per_pipelinerun
+ type: string
+ unmatched_conclusion:
+ description: |-
+ UnmatchedConclusion defines the conclusion to report when pipeline run is not matched. Default is 'skipped'.
+ this will be used only if mode is 'per_pipelinerun'.
+ Options:
+ - 'success': Report as success.
+ - 'neutral': Report as neutral.
+ - 'skipped': Report as skipped. Default.
+ enum:
+ - ""
+ - success
+ - neutral
+ - skipped
+ type: string
+ type: object
type: object
url:
description: |-
diff --git a/docs/content/docs/api/repository-spec.md b/docs/content/docs/api/repository-spec.md
index 67036ef4f8..7369d54719 100644
--- a/docs/content/docs/api/repository-spec.md
+++ b/docs/content/docs/api/repository-spec.md
@@ -307,4 +307,7 @@ spec:
container_logs:
enabled: true
max_lines: 100
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
```
diff --git a/docs/content/docs/api/repository.md b/docs/content/docs/api/repository.md
index f6e2c0a1f6..c72c7cc235 100644
--- a/docs/content/docs/api/repository.md
+++ b/docs/content/docs/api/repository.md
@@ -143,6 +143,9 @@ spec:
container_logs:
enabled: true
max_lines: 100
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
```
## Related resources
diff --git a/docs/content/docs/api/settings.md b/docs/content/docs/api/settings.md
index a55edc99bf..4ae8c0f2aa 100644
--- a/docs/content/docs/api/settings.md
+++ b/docs/content/docs/api/settings.md
@@ -366,6 +366,60 @@ settings:
{{< /param-group >}}
{{< /param >}}
+## Status check settings
+
+{{< param name="status_checks" type="StatusCheck" >}}
+Configures status check reporting for PipelineRuns that did not match the incoming event. See the [Status Checks guide]({{< relref "/docs/guides/repository-crd/status-checks" >}}) for full details and provider behavior.
+
+{{< param-group label="Show StatusCheck Fields" >}}
+
+{{< param name="status_checks.enabled" type="boolean" id="param-status-check-enabled" >}}
+Enables or disables status check reporting for unmatched PipelineRuns. Default: `false`.
+
+```yaml
+settings:
+ status_checks:
+ enabled: true
+```
+
+{{< /param >}}
+
+{{< param name="status_checks.mode" type="string" id="param-status-check-mode" >}}
+Controls how status checks are reported. Options:
+
+- `per_pipelinerun` - Report a separate status for each unmatched PipelineRun
+
+```yaml
+settings:
+ status_checks:
+ mode: "per_pipelinerun"
+```
+
+{{< /param >}}
+
+{{< param name="status_checks.unmatched_conclusion" type="string" id="param-status-check-no-match-conclusion" >}}
+The conclusion to report for unmatched PipelineRuns. Only used when `mode` is `per_pipelinerun`. Default: `skipped`. Options: `skipped`, `success`, `neutral`.
+
+```yaml
+settings:
+ status_checks:
+ unmatched_conclusion: "skipped"
+```
+
+{{< /param >}}
+
+{{< /param-group >}}
+
+```yaml
+settings:
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+ unmatched_conclusion: "skipped"
+```
+
+{{< /param >}}
+
## Complete example
```yaml
@@ -432,6 +486,12 @@ spec:
context_items:
commit_content: true
pr_content: true
+
+ # Status check reporting
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+ unmatched_conclusion: "skipped"
```
## Settings inheritance
diff --git a/docs/content/docs/guides/_index.md b/docs/content/docs/guides/_index.md
index 82aab774a5..b14623ad8c 100644
--- a/docs/content/docs/guides/_index.md
+++ b/docs/content/docs/guides/_index.md
@@ -9,7 +9,7 @@ This section covers the core workflows you need to run CI/CD with Pipelines-as-C
{{< cards >}}
{{< card link="creating-pipelines" title="Authoring PipelineRuns" subtitle="Create pipelines, CEL variables, GitHub token" >}}
- {{< card link="repository-crd" title="Repository CR" subtitle="Configure repos, concurrency, comment settings" >}}
+ {{< card link="repository-crd" title="Repository CR" subtitle="Configure repos, concurrency, comment settings, status checks" >}}
{{< card link="event-matching" title="Event matching" subtitle="on-event, on-target-branch, path, CEL, labels" >}}
{{< card link="gitops-commands" title="GitOps commands" subtitle="/retest, /test, /cancel and more" >}}
{{< card link="statuses" title="PipelineRun status" subtitle="Status reporting and failure detection" >}}
diff --git a/docs/content/docs/guides/repository-crd/status-checks.md b/docs/content/docs/guides/repository-crd/status-checks.md
new file mode 100644
index 0000000000..3c6e002f18
--- /dev/null
+++ b/docs/content/docs/guides/repository-crd/status-checks.md
@@ -0,0 +1,105 @@
+---
+title: Status Checks
+weight: 5
+---
+
+{{< tech_preview "Status Checks settings in Repository CR" >}}
+
+This page explains how to report status checks for PipelineRuns that did not match the incoming event. Use this when you want visibility into which PipelineRuns in your `.tekton/` directory were skipped because their annotations (target branch, event type, CEL expression, or path filter) did not match.
+
+By default, Pipelines-as-Code only reports status for PipelineRuns that matched
+and ran. PipelineRuns that did not match are silently ignored. Enabling
+`status_checks` makes Pipelines-as-Code report a status for each unmatched
+PipelineRun so you can see the full picture in your Git provider's UI.
+
+## Configuration
+
+Add the `status_checks` block under `spec.settings` in your Repository CR:
+
+```yaml
+apiVersion: "pipelinesascode.tekton.dev/v1alpha1"
+kind: Repository
+metadata:
+ name: my-repo
+spec:
+ url: "https://github.com/owner/repo"
+ settings:
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+```
+
+### Fields
+
+| Field | Type | Default | Description |
+| --- | --- | --- | --- |
+| `enabled` | bool | `false` | Enable status check reporting for unmatched PipelineRuns. |
+| `mode` | string | | How to report status checks. See [Modes](#modes). |
+| `unmatched_conclusion` | string | `skipped` | The conclusion to report for unmatched PipelineRuns. Only used when `mode` is `per_pipelinerun`. Accepted values: `skipped`, `success`, `neutral`. |
+
+### Modes
+
+#### `per_pipelinerun`
+
+Reports a separate status for each PipelineRun that did not match the event.
+This is useful when you have multiple PipelineRuns targeting different events
+(for example, one for `pull_request` and one for `push`) and you want to see
+which ones were skipped on each event.
+
+```yaml
+spec:
+ settings:
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+```
+
+### Customizing the conclusion
+
+By default, unmatched PipelineRuns are reported with a `skipped` conclusion.
+You can change this to `success` or `neutral` using the `unmatched_conclusion`
+field:
+
+```yaml
+spec:
+ settings:
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+ unmatched_conclusion: "success"
+```
+
+## Provider behavior
+
+The `skipped` conclusion maps to different states depending on your Git provider:
+
+| Provider | Reported state | Notes |
+| --- | --- | --- |
+| GitHub App | `skipped` | Shown as a skipped check run. |
+| GitHub Webhook | `success` | GitHub commit status API does not support `skipped`. Reported as `success` with a "Skipped" description. |
+| GitLab | `skipped` | Shown as a skipped pipeline in the Pipelines tab. |
+| Bitbucket Cloud | `STOPPED` | Shown as a stopped build status. |
+| Bitbucket Data Center | `UNKNOWN` | Reported with an unknown state. |
+| Gitea / Forgejo | `success` | Gitea does not support `skipped`. Reported as `success` with a "Skipped" description. |
+
+## Example
+
+Consider a repository with two PipelineRuns:
+
+- `.tekton/build.yaml` -- targets `pull_request` events on the `main` branch
+- `.tekton/deploy.yaml` -- targets `push` events on the `main` branch
+
+When a pull request is opened, `build.yaml` matches and runs. Without
+`status_checks`, `deploy.yaml` is silently ignored. With it enabled:
+
+```yaml
+spec:
+ settings:
+ status_checks:
+ enabled: true
+ mode: "per_pipelinerun"
+```
+
+Pipelines-as-Code reports a `skipped` status for `deploy.yaml`, making it
+visible in the pull request's status checks that the PipelineRun exists but did
+not apply to this event.
diff --git a/docs/content/docs/guides/statuses.md b/docs/content/docs/guides/statuses.md
index ea493f0a38..1cbda4db09 100644
--- a/docs/content/docs/guides/statuses.md
+++ b/docs/content/docs/guides/statuses.md
@@ -113,6 +113,10 @@ You can use the `tkn pac describe` command from the [CLI]({{< relref "/docs/cli/
all statuses of PipelineRuns associated with your repository and
their metadata.
+## Status checks for unmatched PipelineRuns
+
+By default, Pipelines-as-Code only reports status for PipelineRuns that matched and ran. If you want to see which PipelineRuns were skipped because their annotations did not match the incoming event, enable the `status_checks` setting in your Repository CR. See the [Status Checks guide]({{< relref "/docs/guides/repository-crd/status-checks" >}}) for configuration and provider behavior.
+
## Notifications
Pipelines-as-Code does not manage notifications directly. Instead, you can add notifications to your PipelineRuns using the [finally feature of
diff --git a/docs/content/docs/operations/global-repository-settings.md b/docs/content/docs/operations/global-repository-settings.md
index ca1caeb390..f3ec0c5bca 100644
--- a/docs/content/docs/operations/global-repository-settings.md
+++ b/docs/content/docs/operations/global-repository-settings.md
@@ -32,6 +32,7 @@ You can define the following settings in the global Repository CR:
- The `type` must be defined in the namespace repository settings and must match the `type` of the global repository (see below for an example).
- [Custom Parameters]({{< relref "/docs/advanced/custom-parameters" >}}).
- [Incoming Webhooks Rules]({{< relref "/docs/advanced/incoming-webhooks" >}}).
+- [Status Checks]({{< relref "/docs/guides/repository-crd/status-checks" >}}).
{{< callout type="info" >}}
Global settings are only applied when running via a Git provider event; they are not applied when for example using the `tkn pac` cli.
diff --git a/pkg/apis/pipelinesascode/v1alpha1/types.go b/pkg/apis/pipelinesascode/v1alpha1/types.go
index 8c205f7d6e..37a592aba3 100644
--- a/pkg/apis/pipelinesascode/v1alpha1/types.go
+++ b/pkg/apis/pipelinesascode/v1alpha1/types.go
@@ -4,6 +4,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
+const (
+ StatusCheckModePerPipelineRun = "per_pipelinerun"
+)
+
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -135,6 +139,10 @@ type Settings struct {
// AIAnalysis contains AI/LLM analysis configuration for automated CI/CD pipeline analysis.
// +optional
AIAnalysis *AIAnalysisConfig `json:"ai,omitempty"`
+
+ // StatusChecks configures the status checks for the repository.
+ // +optional
+ StatusChecks *StatusChecks `json:"status_checks,omitempty"`
}
type GitlabSettings struct {
@@ -182,6 +190,30 @@ type ForgejoSettings struct {
CommentStrategy string `json:"comment_strategy,omitempty"`
}
+// StatusChecks configures the status checks for the repository.
+type StatusChecks struct {
+ // Enabled defines if the status checks should be reported. Default is false.
+ // +optional
+ Enabled bool `json:"enabled,omitempty"`
+
+ // Mode defines how the status checks should be reported when is enabled.
+ // Options:
+ // - 'per_pipelinerun': Report the status check of each PipelineRun separately.
+ // +optional
+ // +kubebuilder:validation:Enum="";per_pipelinerun
+ Mode string `json:"mode,omitempty"`
+
+ // UnmatchedConclusion defines the conclusion to report when pipeline run is not matched. Default is 'skipped'.
+ // this will be used only if mode is 'per_pipelinerun'.
+ // Options:
+ // - 'success': Report as success.
+ // - 'neutral': Report as neutral.
+ // - 'skipped': Report as skipped. Default.
+ // +optional
+ // +kubebuilder:validation:Enum="";success;neutral;skipped
+ UnmatchedConclusion string `json:"unmatched_conclusion,omitempty"`
+}
+
func (s *Settings) Merge(newSettings *Settings) {
if newSettings.PipelineRunProvenance != "" && s.PipelineRunProvenance == "" {
s.PipelineRunProvenance = newSettings.PipelineRunProvenance
@@ -211,6 +243,10 @@ func (s *Settings) Merge(newSettings *Settings) {
if newSettings.GitOpsCommandPrefix != "" && s.GitOpsCommandPrefix == "" {
s.GitOpsCommandPrefix = newSettings.GitOpsCommandPrefix
}
+
+ if newSettings.StatusChecks != nil && s.StatusChecks == nil {
+ s.StatusChecks = newSettings.StatusChecks
+ }
}
func (s *GitlabSettings) Merge(newSettings *GitlabSettings) {
diff --git a/pkg/apis/pipelinesascode/v1alpha1/types_test.go b/pkg/apis/pipelinesascode/v1alpha1/types_test.go
index de8df17e9d..6b0276fc72 100644
--- a/pkg/apis/pipelinesascode/v1alpha1/types_test.go
+++ b/pkg/apis/pipelinesascode/v1alpha1/types_test.go
@@ -343,6 +343,92 @@ func TestMergeSpecs(t *testing.T) {
},
},
},
+ {
+ name: "status check from global",
+ local: &RepositorySpec{
+ Settings: &Settings{},
+ GitProvider: &GitProvider{},
+ },
+ global: RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "skipped",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ expected: &RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "skipped",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ },
+ {
+ name: "local status check takes precedence",
+ local: &RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "skipped",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ global: RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "neutral",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ expected: &RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "skipped",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ },
+ {
+ name: "nil local settings inherits global status check",
+ local: &RepositorySpec{
+ GitProvider: &GitProvider{},
+ },
+ global: RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "success",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ expected: &RepositorySpec{
+ Settings: &Settings{
+ StatusChecks: &StatusChecks{
+ Enabled: true,
+ Mode: "per_pipelinerun",
+ UnmatchedConclusion: "success",
+ },
+ },
+ GitProvider: &GitProvider{},
+ },
+ },
}
for _, tt := range tests {
diff --git a/pkg/apis/pipelinesascode/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipelinesascode/v1alpha1/zz_generated.deepcopy.go
index 3483c6b410..64952a3141 100644
--- a/pkg/apis/pipelinesascode/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipelinesascode/v1alpha1/zz_generated.deepcopy.go
@@ -410,6 +410,11 @@ func (in *Settings) DeepCopyInto(out *Settings) {
*out = new(AIAnalysisConfig)
(*in).DeepCopyInto(*out)
}
+ if in.StatusChecks != nil {
+ in, out := &in.StatusChecks, &out.StatusChecks
+ *out = new(StatusChecks)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Settings.
@@ -422,6 +427,21 @@ func (in *Settings) DeepCopy() *Settings {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StatusChecks) DeepCopyInto(out *StatusChecks) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusChecks.
+func (in *StatusChecks) DeepCopy() *StatusChecks {
+ if in == nil {
+ return nil
+ }
+ out := new(StatusChecks)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TaskInfos) DeepCopyInto(out *TaskInfos) {
*out = *in
diff --git a/pkg/matcher/annotation_matcher.go b/pkg/matcher/annotation_matcher.go
index f9a7d95086..128377ec6a 100644
--- a/pkg/matcher/annotation_matcher.go
+++ b/pkg/matcher/annotation_matcher.go
@@ -208,8 +208,9 @@ func checkPipelineRunAnnotation(prun *tektonv1.PipelineRun, eventEmitter *events
}
}
-func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger, pruns []*tektonv1.PipelineRun, cs *params.Run, event *info.Event, vcx provider.Interface, eventEmitter *events.EventEmitter, repo *apipac.Repository, reportErrors bool) ([]Match, error) {
+func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger, pruns []*tektonv1.PipelineRun, cs *params.Run, event *info.Event, vcx provider.Interface, eventEmitter *events.EventEmitter, repo *apipac.Repository, reportErrors bool) ([]Match, map[string]*tektonv1.PipelineRun, error) {
matchedPRs := []Match{}
+ unmatchedPRs := make(map[string]*tektonv1.PipelineRun, len(pruns))
logger.Debugf("MatchPipelinerunByAnnotation: pipelineruns=%d event_type=%s trigger_target=%s report_errors=%t", len(pruns), event.EventType, event.TriggerTarget, reportErrors)
infomsg := fmt.Sprintf(
"matching pipelineruns to event: URL=%s, target-branch=%s, source-branch=%s, target-event=%s",
@@ -302,6 +303,8 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
matchedPRs = append(matchedPRs, prMatch)
continue
}
+ logger.Debugf("PipelineRun %s: on-comment annotation did not match, skipping", prName)
+ unmatchedPRs[prName] = prun
}
// if the event is a comment event, but we don't have any match from the keys.OnComment then skip the other evaluations
if event.EventType == opscomments.NoOpsCommentEventType.String() || event.EventType == opscomments.OnCommentEventType.String() {
@@ -342,6 +345,9 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
logger.Debugf("PipelineRun %s: CEL result=%v", prName, out)
if out != types.True {
logger.Infof("CEL expression for PipelineRun %s is not matching, skipping", prName)
+ if _, ok := unmatchedPRs[prName]; !ok {
+ unmatchedPRs[prName] = prun
+ }
continue
}
logger.Infof("CEL expression has been evaluated and matched")
@@ -357,10 +363,13 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
matched, targetEvent, targetBranch, err := getTargetBranch(prun, event)
if err != nil {
- return matchedPRs, err
+ return matchedPRs, unmatchedPRs, err
}
if !matched {
logger.Debugf("PipelineRun %s: target branch/event did not match", prName)
+ if _, ok := unmatchedPRs[prName]; !ok {
+ unmatchedPRs[prName] = prun
+ }
continue
}
prMatch.Config["target-branch"] = targetBranch
@@ -378,10 +387,13 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
// our own path changes. we may split up if needed to refine.
matched, err := matchOnAnnotation(key, changedFiles.All, true)
if err != nil {
- return matchedPRs, err
+ return matchedPRs, unmatchedPRs, err
}
if !matched {
logger.Debugf("PipelineRun %s: path-change annotation did not match", prName)
+ if _, ok := unmatchedPRs[prName]; !ok {
+ unmatchedPRs[prName] = prun
+ }
continue
}
logger.Infof("matched PipelineRun with name: %s, annotation PathChange: %q", prName, key)
@@ -391,10 +403,13 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
if key, ok := prun.GetObjectMeta().GetAnnotations()[keys.OnLabel]; ok {
matched, err := matchOnAnnotation(key, event.PullRequestLabel, false)
if err != nil {
- return matchedPRs, err
+ return matchedPRs, unmatchedPRs, err
}
if !matched {
- logger.Debugf("PipelineRun %s: label annotation did not match", prName)
+ logger.Debugf("PipelineRun %s: label annotation did not match, skipping", prName)
+ if _, ok := unmatchedPRs[prName]; !ok {
+ unmatchedPRs[prName] = prun
+ }
continue
}
logger.Infof("matched PipelineRun with name: %s, annotation Label: %q", prName, key)
@@ -412,10 +427,13 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
// our own path changes. we may split up if needed to refine.
matched, err := matchOnAnnotation(key, changedFiles.All, true)
if err != nil {
- return matchedPRs, err
+ return matchedPRs, unmatchedPRs, err
}
if matched {
logger.Infof("Skipping pipelinerun with name: %s, annotation PathChangeIgnore: %q", prName, key)
+ if _, ok := unmatchedPRs[prName]; !ok {
+ unmatchedPRs[prName] = prun
+ }
continue
}
prMatch.Config["path-change-ignore"] = key
@@ -439,14 +457,14 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
logger.Debugf("MatchPipelinerunByAnnotation: filtering successful templates for event_type=%s", event.EventType)
filtered := filterSuccessfulTemplates(ctx, logger, cs, event, repo, vcx, matchedPRs)
if len(filtered) == 0 {
- return nil, NoFailedPipelineToRetestError(provider.GetGitOpsCommentPrefix(repo))
+ return nil, unmatchedPRs, NoFailedPipelineToRetestError(provider.GetGitOpsCommentPrefix(repo))
}
- return filtered, nil
+ return filtered, unmatchedPRs, nil
}
- return matchedPRs, nil
+ return matchedPRs, unmatchedPRs, nil
}
- return nil, fmt.Errorf("%s", buildAvailableMatchingAnnotationErr(event, pruns))
+ return nil, unmatchedPRs, fmt.Errorf("%s", buildAvailableMatchingAnnotationErr(event, pruns))
}
// filterSuccessfulTemplates filters out templates that already have successful PipelineRuns
diff --git a/pkg/matcher/annotation_matcher_test.go b/pkg/matcher/annotation_matcher_test.go
index bfcb5a0438..4b5156454f 100644
--- a/pkg/matcher/annotation_matcher_test.go
+++ b/pkg/matcher/annotation_matcher_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"reflect"
+ "slices"
"strings"
"testing"
"time"
@@ -1699,7 +1700,7 @@ func runTest(ctx context.Context, t *testing.T, tt annotationTest, vcx provider.
repo = tt.args.data.Repositories[0]
}
- matches, err := MatchPipelinerunByAnnotation(
+ matches, _, err := MatchPipelinerunByAnnotation(
ctx, logger,
tt.args.pruns,
client, &tt.args.runevent, vcx, eventEmitter, repo, true,
@@ -1747,6 +1748,15 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
},
}
+ pipelinePushCel := &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pipeline-push-cel",
+ Annotations: map[string]string{
+ keys.OnCelExpression: `event == "push"`,
+ },
+ },
+ }
+
pipelinePush := &tektonv1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline-push",
@@ -1766,6 +1776,17 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
},
}
+ pipelineOnLabel := &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pipeline-on-label",
+ Annotations: map[string]string{
+ keys.OnLabel: "[bug]",
+ keys.OnEvent: "[pull_request]",
+ keys.OnTargetBranch: "[main]",
+ },
+ },
+ }
+
pipelineOther := &tektonv1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline-other",
@@ -1806,7 +1827,7 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
},
}
- observer, log := zapobserver.New(zap.InfoLevel)
+ observer, log := zapobserver.New(zap.DebugLevel)
logger := zap.New(observer).Sugar()
pipelinePullRequestForRetest := &tektonv1.PipelineRun{
@@ -1828,8 +1849,8 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
args args
wantErr bool
wantPrName string
+ wantUnmatchedPRs []string
wantLog []string
- logLevel int
repo *v1alpha1.Repository
seedData *testclient.Data
wantErrNoFailedPipelineToRetest bool
@@ -2342,6 +2363,68 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
}},
},
},
+ {
+ name: "good-match-with-only-one-unmatched-pipeline-run",
+ args: args{
+ pruns: []*tektonv1.PipelineRun{pipelineGood, pipelinePushCel},
+ runevent: info.Event{
+ URL: "https://hello/moto",
+ TriggerTarget: "pull_request",
+ EventType: "pull_request",
+ HeadBranch: "source",
+ BaseBranch: "main",
+ PullRequestNumber: 10,
+ Request: &info.Request{
+ Header: http.Header{},
+ },
+ },
+ },
+ wantErr: false,
+ wantUnmatchedPRs: []string{"pipeline-push-cel"},
+ wantLog: []string{"CEL expression for PipelineRun pipeline-push-cel is not matching, skipping"},
+ },
+ {
+ name: "good-match-with-only-one-unmatched-pipeline-run-on-comment",
+ args: args{
+ pruns: []*tektonv1.PipelineRun{pipelineOnComment},
+ runevent: info.Event{
+ URL: "https://hello/moto",
+ TriggerTarget: "pull_request",
+ EventType: "no-ops-comment",
+ HeadBranch: "source",
+ BaseBranch: "main",
+ PullRequestNumber: 10,
+ TriggerComment: "/bye-world",
+ Request: &info.Request{
+ Header: http.Header{},
+ },
+ },
+ },
+ wantErr: true,
+ wantUnmatchedPRs: []string{"pipeline-on-comment"},
+ wantLog: []string{"PipelineRun pipeline-on-comment: on-comment annotation did not match, skipping"},
+ },
+ {
+ name: "good-match-with-only-one-unmatched-pipeline-run-on-label",
+ args: args{
+ pruns: []*tektonv1.PipelineRun{pipelineOnLabel},
+ runevent: info.Event{
+ URL: "https://hello/moto",
+ TriggerTarget: "pull_request",
+ EventType: "pull_request_labeled",
+ HeadBranch: "source",
+ BaseBranch: "main",
+ PullRequestNumber: 10,
+ PullRequestLabel: []string{"feature"},
+ Request: &info.Request{
+ Header: http.Header{},
+ },
+ },
+ },
+ wantErr: true,
+ wantUnmatchedPRs: []string{"pipeline-on-label"},
+ wantLog: []string{"PipelineRun pipeline-on-label: label annotation did not match, skipping"},
+ },
}
for _, tt := range tests {
@@ -2359,7 +2442,7 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
eventEmitter := events.NewEventEmitter(cs.Clients.Kube, logger)
repo := tt.repo
- matches, err := MatchPipelinerunByAnnotation(ctx, logger, tt.args.pruns, cs, &tt.args.runevent, &ghprovider.Provider{}, eventEmitter, repo, true)
+ matches, unmatchedPRs, err := MatchPipelinerunByAnnotation(ctx, logger, tt.args.pruns, cs, &tt.args.runevent, &ghprovider.Provider{}, eventEmitter, repo, true)
if tt.wantErrNoFailedPipelineToRetest {
assert.Assert(t, err != nil, "expected ErrNoFailedPipelineToRetest")
assert.Assert(t, errors.Is(err, NoFailedPipelineToRetestError("/pac ")), "expected ErrNoFailedPipelineToRetest, got: %v", err)
@@ -2376,6 +2459,13 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
assert.Assert(t, matches[0].PipelineRun.GetName() == tt.wantPrName, "Pipelinerun hasn't been matched: %+v",
matches[0].PipelineRun.GetName(), tt.wantPrName)
}
+
+ if len(tt.wantUnmatchedPRs) > 0 {
+ assert.Assert(t, len(unmatchedPRs) == len(tt.wantUnmatchedPRs), "expected %d unmatched pipelineruns, got %d", len(tt.wantUnmatchedPRs), len(unmatchedPRs))
+ for _, unmatchedPR := range unmatchedPRs {
+ assert.Assert(t, slices.Contains(tt.wantUnmatchedPRs, unmatchedPR.GetName()), "unmatched pipelinerun %s not in expected list", unmatchedPR.GetName())
+ }
+ }
if len(tt.wantLog) > 0 {
assert.Assert(t, log.Len() > 0, "We didn't get any log message")
all := log.TakeAll()
diff --git a/pkg/pipelineascode/match.go b/pkg/pipelineascode/match.go
index db3df2f451..cba0819cda 100644
--- a/pkg/pipelineascode/match.go
+++ b/pkg/pipelineascode/match.go
@@ -25,30 +25,30 @@ import (
"go.uber.org/zap"
)
-func (p *PacRun) matchRepoPR(ctx context.Context) ([]matcher.Match, *v1alpha1.Repository, error) {
+func (p *PacRun) matchRepoPR(ctx context.Context) ([]matcher.Match, map[string]*tektonv1.PipelineRun, *v1alpha1.Repository, error) {
p.debugf("matchRepoPR: starting repo verification for url=%s", p.event.URL)
repo, err := p.verifyRepoAndUser(ctx)
if err != nil {
- return nil, nil, err
+ return nil, nil, repo, err
}
if repo == nil {
p.debugf("matchRepoPR: no repository match for url=%s", p.event.URL)
- return nil, nil, nil
+ return nil, nil, repo, nil
}
if p.event.CancelPipelineRuns {
p.debugf("matchRepoPR: cancel pipeline runs requested, skipping match")
- return nil, repo, p.cancelPipelineRunsOpsComment(ctx, repo)
+ return nil, nil, repo, p.cancelPipelineRunsOpsComment(ctx, repo)
}
p.debugf("matchRepoPR: fetching pipelineruns from repo=%s/%s", repo.GetNamespace(), repo.GetName())
- matchedPRs, err := p.getPipelineRunsFromRepo(ctx, repo)
+ matchedPRs, unmatchedPRs, err := p.getPipelineRunsFromRepo(ctx, repo)
if err != nil {
- return nil, repo, err
+ return nil, nil, repo, err
}
p.debugf("matchRepoPR: matched=%d repo=%s/%s", len(matchedPRs), repo.GetNamespace(), repo.GetName())
- return matchedPRs, repo, nil
+ return matchedPRs, unmatchedPRs, repo, nil
}
// verifyRepoAndUser verifies if the Repo CR exists for the Git Repository,
@@ -143,7 +143,7 @@ func (p *PacRun) verifyRepoAndUser(ctx context.Context) (*v1alpha1.Repository, e
}
// getPipelineRunsFromRepo fetches pipelineruns from git repository and prepare them for creation.
-func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Repository) ([]matcher.Match, error) {
+func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Repository) ([]matcher.Match, map[string]*tektonv1.PipelineRun, error) {
provenance := "source"
if repo.Spec.Settings != nil && repo.Spec.Settings.PipelineRunProvenance != "" {
provenance = repo.Spec.Settings.PipelineRunProvenance
@@ -168,10 +168,10 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
},
},
)
- return nil, nil
+ return nil, nil, nil
}
- return nil, err
+ return nil, nil, err
}
if err != nil {
@@ -203,14 +203,14 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
msg = fmt.Sprintf("cannot locate templates in %s/ directory for this repository in %s", tektonDir, p.event.HeadBranch)
}
p.eventEmitter.EmitMessage(nil, logLevel, reason, msg)
- return nil, nil
+ return nil, nil, nil
}
// check for condition if need update the pipelinerun with regexp from the
// "raw" pipelinerun string
if msg, needUpdate := p.checkNeedUpdate(rawTemplates); needUpdate {
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryNeedUpdate", msg)
- return nil, fmt.Errorf("%s", msg)
+ return nil, nil, fmt.Errorf("%s", msg)
}
// This is for bitbucket
@@ -232,11 +232,11 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
if p.event.TargetTestPipelineRun == "" {
rtypes, err := resolve.ReadTektonTypes(ctx, p.logger, rawTemplates)
if err != nil {
- return nil, err
+ return nil, nil, err
}
p.debugf("getPipelineRunsFromRepo: pre-parse types: pipelineruns=%d pipelines=%d tasks=%d", len(rtypes.PipelineRuns), len(rtypes.Pipelines), len(rtypes.Tasks))
// Don't fail or do anything if we don't have a match yet, we will do it properly later in this function
- _, _ = matcher.MatchPipelinerunByAnnotation(ctx, p.logger, rtypes.PipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, false)
+ _, _, _ = matcher.MatchPipelinerunByAnnotation(ctx, p.logger, rtypes.PipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, false)
}
// Replace those {{var}} placeholders user has in her template to the run.Info variable
allTemplates := p.makeTemplate(ctx, repo, rawTemplates)
@@ -244,7 +244,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
types, err := resolve.ReadTektonTypes(ctx, p.logger, allTemplates)
if err != nil {
- return nil, err
+ return nil, nil, err
}
p.debugf("getPipelineRunsFromRepo: parsed types: pipelineruns=%d pipelines=%d tasks=%d validation_errors=%d", len(types.PipelineRuns), len(types.Pipelines), len(types.Tasks), len(types.ValidationErrors))
@@ -255,7 +255,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
if len(pipelineRuns) == 0 {
msg := fmt.Sprintf("cannot locate valid templates in %s/ directory for this repository in %s", tektonDir, p.event.HeadBranch)
p.eventEmitter.EmitMessage(nil, zap.InfoLevel, "RepositoryCannotLocatePipelineRun", msg)
- return nil, nil
+ return nil, nil, nil
}
p.debugf("getPipelineRunsFromRepo: pipelineRuns count=%d", len(pipelineRuns))
pipelineRuns, err = resolve.MetadataResolve(pipelineRuns)
@@ -264,26 +264,27 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
// reporting creates a comment, which triggers another webhook, which hits the same error.
if p.event.EventType == opscomments.NoOpsCommentEventType.String() {
p.logger.Infof("skipping MetadataResolve error for no-ops comment event: %s", err)
- return nil, nil
+ return nil, nil, nil
}
p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "FailedToResolvePipelineRunMetadata", err.Error())
- return nil, err
+ return nil, nil, err
}
p.debugf("getPipelineRunsFromRepo: metadata resolved for pipelineRuns count=%d", len(pipelineRuns))
// Match the PipelineRun with annotation
var matchedPRs []matcher.Match
+ var unmatchedPRs map[string]*tektonv1.PipelineRun
if p.event.TargetTestPipelineRun == "" {
- if matchedPRs, err = matcher.MatchPipelinerunByAnnotation(ctx, p.logger, pipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, true); err != nil {
+ if matchedPRs, unmatchedPRs, err = matcher.MatchPipelinerunByAnnotation(ctx, p.logger, pipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, true); err != nil {
prefix := provider.GetGitOpsCommentPrefix(repo)
// Check if all pipelines have already succeeded - post comment so user gets feedback
if errors.Is(err, matcher.NoFailedPipelineToRetestError(prefix)) {
p.logger.Infof("RepositoryAllPipelinesSucceeded: %s", err.Error())
p.eventEmitter.EmitMessage(nil, zap.InfoLevel, "RepositoryAllPipelinesSucceeded", err.Error())
if commentErr := p.vcx.CreateComment(ctx, p.event, err.Error(), ""); commentErr != nil {
- return nil, fmt.Errorf("error adding no pipelineruns to rerun comment: %w", commentErr)
+ return nil, nil, fmt.Errorf("error adding no pipelineruns to rerun comment: %w", commentErr)
}
- return nil, nil
+ return nil, unmatchedPRs, nil
}
// Don't fail when you don't have a match between pipeline and annotations
p.eventEmitter.EmitMessage(nil, zap.WarnLevel, "RepositoryNoMatch", err.Error())
@@ -298,7 +299,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
}
p.eventEmitter.EmitMessage(nil, zap.InfoLevel, "RepositoryNoMatch", text)
}
- return nil, nil
+ return nil, unmatchedPRs, nil
}
p.debugf("getPipelineRunsFromRepo: initial match count=%d", len(matchedPRs))
}
@@ -314,7 +315,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
AccessDenied: true,
}
if allowed, err := p.checkAccessOrError(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
- return nil, err
+ return nil, nil, err
}
}
@@ -323,7 +324,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
if pipelineRuns == nil {
msg := fmt.Sprintf("cannot find pipelinerun %s for matching an incoming event in this repository", p.event.TargetPipelineRun)
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryCannotLocatePipelineRunForIncomingEvent", msg)
- return nil, nil
+ return nil, nil, nil
}
p.debugf("getPipelineRunsFromRepo: incoming filter result count=%d", len(pipelineRuns))
@@ -333,7 +334,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
if targetPR == nil {
msg := fmt.Sprintf("cannot find the targeted pipelinerun %s in this repository", p.event.TargetTestPipelineRun)
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryCannotLocatePipelineRun", msg)
- return nil, nil
+ return nil, nil, nil
}
pipelineRuns = []*tektonv1.PipelineRun{targetPR}
p.debugf("getPipelineRunsFromRepo: filtered to target pipelinerun=%s", p.event.TargetTestPipelineRun)
@@ -361,13 +362,13 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
})
if err != nil {
p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "RepositoryFailedToMatch", fmt.Sprintf("failed to match pipelineRuns: %s", err.Error()))
- return nil, err
+ return nil, nil, err
}
}
err = p.changePipelineRun(ctx, repo, pipelineRuns)
if err != nil {
- return nil, err
+ return nil, nil, err
}
p.debugf("getPipelineRunsFromRepo: updated pipelineRuns count=%d", len(pipelineRuns))
// if we are doing explicit /test command then we only want to run the one that has matched the /test
@@ -377,30 +378,35 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
if selectedPr == nil {
msg := fmt.Sprintf("cannot find the targeted pipelinerun %s in this repository", p.event.TargetTestPipelineRun)
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryCannotLocatePipelineRun", msg)
- return nil, nil
+ return nil, nil, nil
}
selectedRepo := p.resolveTargetNamespaceRepo(ctx, repo, selectedPr)
if selectedRepo == nil {
msg := fmt.Sprintf("skipping pipelinerun %s: target-namespace repo not found", pipelineRunIdentifier(selectedPr))
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryTargetNamespaceNotFound", msg)
- return nil, nil
+ return nil, nil, nil
}
p.debugf("getPipelineRunsFromRepo: explicit /test using repo=%s/%s for pipelinerun=%s", selectedRepo.GetNamespace(), selectedRepo.GetName(), pipelineRunIdentifier(selectedPr))
return []matcher.Match{{
PipelineRun: selectedPr,
Repo: selectedRepo,
- }}, nil
+ }}, nil, nil
}
- matchedPRs, err = matcher.MatchPipelinerunByAnnotation(ctx, p.logger, pipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, false)
+ // unmatchedPRs are filtered out above if RemoteTasks is enabled so we should also check if RemoteTasks is enabled then
+ // return unmatchedPRs from last call to MatchPipelinerunByAnnotation and if RemoteTasks is disabled then return unmatchedPRs from here
+ matchedPRs, unmatchedPRsNew, err := matcher.MatchPipelinerunByAnnotation(ctx, p.logger, pipelineRuns, p.run, p.event, p.vcx, p.eventEmitter, repo, false)
+ if !p.pacInfo.RemoteTasks {
+ unmatchedPRs = unmatchedPRsNew
+ }
if err != nil {
// Don't fail when you don't have a match between pipeline and annotations
p.eventEmitter.EmitMessage(nil, zap.WarnLevel, "RepositoryNoMatch", err.Error())
- return nil, nil
+ return nil, unmatchedPRs, nil
}
p.debugf("getPipelineRunsFromRepo: final match count=%d", len(matchedPRs))
- return matchedPRs, nil
+ return matchedPRs, unmatchedPRs, nil
}
func getRepositoryRevisionForProvenance(event *info.Event, provenance string) string {
diff --git a/pkg/pipelineascode/match_test.go b/pkg/pipelineascode/match_test.go
index 7d1b450bb2..bd2e9b6685 100644
--- a/pkg/pipelineascode/match_test.go
+++ b/pkg/pipelineascode/match_test.go
@@ -317,7 +317,7 @@ spec:
nil,
)
- matchedPRs, err := p.getPipelineRunsFromRepo(ctx, repositories[0])
+ matchedPRs, _, err := p.getPipelineRunsFromRepo(ctx, repositories[0])
assert.NilError(t, err)
if tt.wantNoMatch {
assert.Equal(t, len(matchedPRs), 0)
@@ -453,7 +453,7 @@ spec:
p := NewPacs(event, vcx, cs, pacInfo, nil, logger, nil)
p.eventEmitter = events.NewEventEmitter(stdata.Kube, logger)
- matchedPRs, err := p.getPipelineRunsFromRepo(ctx, repo)
+ matchedPRs, _, err := p.getPipelineRunsFromRepo(ctx, repo)
assert.NilError(t, err)
assert.Equal(t, 1, len(matchedPRs))
assert.Equal(t, tt.wantRevision, vcx.fileInsideRepoRevision)
@@ -523,14 +523,15 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
}
tests := []struct {
- name string
- repositories *v1alpha1.Repository
- tektondir string
- expectedNumberOfPruns int
- event *info.Event
- logSnippet string
- wantErr bool
- seedData *testclient.Data
+ name string
+ repositories *v1alpha1.Repository
+ tektondir string
+ expectedNumberOfPruns int
+ expectedNumberOfUnmatchedPruns int
+ event *info.Event
+ logSnippet string
+ wantErr bool
+ seedData *testclient.Data
}{
{
name: "more than one pipelinerun in .tekton dir",
@@ -541,9 +542,10 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
},
Spec: v1alpha1.RepositorySpec{},
},
- tektondir: "testdata/pull_request_multiplepipelineruns",
- expectedNumberOfPruns: 2,
- event: pullRequestEvent,
+ tektondir: "testdata/pull_request_multiplepipelineruns",
+ expectedNumberOfPruns: 2,
+ expectedNumberOfUnmatchedPruns: 1,
+ event: pullRequestEvent,
},
{
name: "single pipelinerun in .tekton dir",
@@ -582,9 +584,10 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
},
// we have 3 PR in there 2 that has a match on pull request and 1 that is a no-matching
// matching those two that is matching here
- tektondir: "testdata/no-match",
- expectedNumberOfPruns: 2,
- event: pullRequestEvent,
+ tektondir: "testdata/no-match",
+ expectedNumberOfPruns: 2,
+ expectedNumberOfUnmatchedPruns: 1,
+ event: pullRequestEvent,
},
{
name: "no-match pipelineruns in .tekton dir, only match the no-match",
@@ -613,9 +616,10 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
// if `testdata/no_yaml` dir is supplied here p.getPipelineRunsFromRepo func will return after
// GetTektonDir so providing `testdat/push_branch` so that it should call MatchPipelineRunsByAnnotation
// first and then create a neutral check-run.
- tektondir: "testdata/push_branch",
- expectedNumberOfPruns: 0,
- event: okToTestEvent,
+ tektondir: "testdata/push_branch",
+ expectedNumberOfPruns: 0,
+ expectedNumberOfUnmatchedPruns: 1,
+ event: okToTestEvent,
},
{
name: "no .tekton dir in repository",
@@ -751,7 +755,7 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
vcx.SetPacInfo(pacInfo)
p := NewPacs(tt.event, vcx, cs, pacInfo, k8int, logger, nil)
p.eventEmitter = events.NewEventEmitter(stdata.Kube, logger)
- matchedPRs, err := p.getPipelineRunsFromRepo(ctx, tt.repositories)
+ matchedPRs, unmatchedPRs, err := p.getPipelineRunsFromRepo(ctx, tt.repositories)
if tt.wantErr {
assert.Assert(t, err != nil, "expected an error but got nil")
return
@@ -765,6 +769,7 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
assert.Assert(t, logCatcher.FilterMessageSnippet(tt.logSnippet).Len() > 0, logCatcher.All())
}
assert.Equal(t, len(matchedPRNames), tt.expectedNumberOfPruns)
+ assert.Equal(t, len(unmatchedPRs), tt.expectedNumberOfUnmatchedPruns)
})
}
}
diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go
index 1808c6e722..1d1ca06877 100644
--- a/pkg/pipelineascode/pipelineascode.go
+++ b/pkg/pipelineascode/pipelineascode.go
@@ -81,7 +81,7 @@ func (p *PacRun) Run(ctx context.Context) error {
return nil
}
- matchedPRs, repo, err := p.matchRepoPR(ctx)
+ matchedPRs, unmatchedPRs, repo, err := p.matchRepoPR(ctx)
if err != nil {
createStatusErr := p.vcx.CreateStatus(ctx, p.event, providerstatus.StatusOpts{
Status: CompletedStatus,
@@ -103,6 +103,10 @@ func (p *PacRun) Run(ctx context.Context) error {
p.debugf("match results: matched=%d repo=%s/%s", len(matchedPRs), repoNamespace, repoName)
if len(matchedPRs) == 0 {
p.debugf("no pipelineruns matched; returning without starting any runs")
+ // check and report if status check is enabled from repo CR settings here so that status reporting will be done early
+ // if there is no matched PipelineRun otherwise after all the pipelineruns are started, we will report the status check
+ // for all the unmatched pipelineruns, to not cause delay to matched PipelineRuns start and reporting process.
+ p.reportStatusCheckFromRepoSettings(ctx, repo, unmatchedPRs, "when there is no matched pipelinerun")
return nil
}
if repo == nil {
@@ -204,9 +208,57 @@ func (p *PacRun) Run(ctx context.Context) error {
}
}
wg.Wait()
+ // report status check for unmatched pipelineruns after all the pipelineruns are started
+ p.reportStatusCheckFromRepoSettings(ctx, repo, unmatchedPRs, "after all the pipelineruns are started")
return nil
}
+func (p *PacRun) reportStatusCheckFromRepoSettings(ctx context.Context, repo *v1alpha1.Repository, unmatchedPRs map[string]*tektonv1.PipelineRun, whenMsg string) {
+ p.debugf("checking status check settings from repo CR %s", whenMsg)
+ // since only per_pipelinerun is supported now, we default to it if mode is not set but enabled is true
+ if repo != nil && repo.Spec.Settings != nil && repo.Spec.Settings.StatusChecks != nil &&
+ repo.Spec.Settings.StatusChecks.Enabled &&
+ (repo.Spec.Settings.StatusChecks.Mode == v1alpha1.StatusCheckModePerPipelineRun || repo.Spec.Settings.StatusChecks.Mode == "") {
+ p.debugf("status check is enabled from repo CR settings in mode=%s for %d unmatched pipelineruns", repo.Spec.Settings.StatusChecks.Mode, len(unmatchedPRs))
+ } else {
+ return
+ }
+
+ conclusion := providerstatus.Conclusion(repo.Spec.Settings.StatusChecks.UnmatchedConclusion)
+ if conclusion == "" {
+ conclusion = providerstatus.ConclusionSkipped
+ }
+ p.debugf("reporting status check from repo settings for %d unmatched pipelineruns with conclusion=%s", len(unmatchedPRs), conclusion)
+
+ var wg sync.WaitGroup
+ for _, pr := range unmatchedPRs {
+ wg.Add(1)
+
+ go func(pr *tektonv1.PipelineRun) {
+ defer wg.Done()
+ prName := pr.GetName()
+ if prName == "" {
+ prName = pr.GetGenerateName()
+ }
+ err := p.vcx.CreateStatus(ctx, p.event, providerstatus.StatusOpts{
+ PipelineRunName: prName,
+ IsUnmatchedReport: true,
+ PipelineRun: pr,
+ OriginalPipelineRunName: prName,
+ DetailsURL: p.run.Clients.ConsoleUI().URL(),
+ Status: CompletedStatus,
+ Conclusion: conclusion,
+ Text: fmt.Sprintf("PipelineRun %s is not matched to event %s", prName, p.event.TriggerTarget.String()),
+ })
+ // we don't return the error here because we want to report all the status checks
+ if err != nil {
+ p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "RepositoryStatusCheckReportFailed", fmt.Sprintf("error reporting status check from repo settings: %s", err.Error()))
+ }
+ }(pr)
+ }
+ wg.Wait()
+}
+
func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.PipelineRun, error) {
prName := match.PipelineRun.GetName()
if prName == "" {
diff --git a/pkg/pipelineascode/pipelineascode_statuscheck_test.go b/pkg/pipelineascode/pipelineascode_statuscheck_test.go
new file mode 100644
index 0000000000..9b0d6dbac1
--- /dev/null
+++ b/pkg/pipelineascode/pipelineascode_statuscheck_test.go
@@ -0,0 +1,182 @@
+package pipelineascode
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "testing"
+
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/consoleui"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/events"
+ "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/triggertype"
+ providerstatus "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/status"
+ testclient "github.com/openshift-pipelines/pipelines-as-code/pkg/test/clients"
+ testprovider "github.com/openshift-pipelines/pipelines-as-code/pkg/test/provider"
+ tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
+ "go.uber.org/zap"
+ zapobserver "go.uber.org/zap/zaptest/observer"
+ "gotest.tools/v3/assert"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ rtesting "knative.dev/pkg/reconciler/testing"
+)
+
+type statusCapturingProvider struct {
+ testprovider.TestProviderImp
+ mu sync.Mutex
+ statuses []providerstatus.StatusOpts
+}
+
+func (s *statusCapturingProvider) CreateStatus(_ context.Context, _ *info.Event, opts providerstatus.StatusOpts) error {
+ if s.CreateStatusErorring {
+ return fmt.Errorf("some provider error occurred while reporting status")
+ }
+ s.mu.Lock()
+ s.statuses = append(s.statuses, opts)
+ s.mu.Unlock()
+ return nil
+}
+
+func TestReportStatusCheckFromRepoSettings(t *testing.T) {
+ tests := []struct {
+ name string
+ noMatchConclusion string
+ unmatchedPRs map[string]*tektonv1.PipelineRun
+ createStatusErorring bool
+ expectedConclusion providerstatus.Conclusion
+ expectedStatusCount int
+ expectedLogSnippet string
+ }{
+ {
+ name: "default conclusion is skipped when not set",
+ noMatchConclusion: "",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "pr-one": {ObjectMeta: metav1.ObjectMeta{GenerateName: "pr-one-"}},
+ },
+ expectedConclusion: providerstatus.ConclusionSkipped,
+ expectedStatusCount: 1,
+ },
+ {
+ name: "custom conclusion success",
+ noMatchConclusion: "success",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "pr-one": {ObjectMeta: metav1.ObjectMeta{GenerateName: "pr-one-"}},
+ },
+ expectedConclusion: providerstatus.ConclusionSuccess,
+ expectedStatusCount: 1,
+ },
+ {
+ name: "custom conclusion neutral",
+ noMatchConclusion: "neutral",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "pr-with-name": {ObjectMeta: metav1.ObjectMeta{Name: "pr-with-name"}},
+ },
+ expectedConclusion: providerstatus.ConclusionNeutral,
+ expectedStatusCount: 1,
+ },
+ {
+ name: "multiple unmatched pipelineruns",
+ noMatchConclusion: "skipped",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "pr-one": {ObjectMeta: metav1.ObjectMeta{GenerateName: "pr-one-"}},
+ "pr-two": {ObjectMeta: metav1.ObjectMeta{GenerateName: "pr-two-"}},
+ "pr-three": {ObjectMeta: metav1.ObjectMeta{Name: "pr-three"}},
+ },
+ expectedConclusion: providerstatus.ConclusionSkipped,
+ expectedStatusCount: 3,
+ },
+ {
+ name: "empty unmatched pipelineruns",
+ noMatchConclusion: "skipped",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{},
+ expectedStatusCount: 0,
+ },
+ {
+ name: "uses name over generateName",
+ noMatchConclusion: "skipped",
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "my-named-pr": {ObjectMeta: metav1.ObjectMeta{Name: "my-named-pr", GenerateName: "should-not-use-"}},
+ },
+ expectedConclusion: providerstatus.ConclusionSkipped,
+ expectedStatusCount: 1,
+ },
+ {
+ name: "create status error emits log message",
+ noMatchConclusion: "skipped",
+ createStatusErorring: true,
+ unmatchedPRs: map[string]*tektonv1.PipelineRun{
+ "pr-fail": {ObjectMeta: metav1.ObjectMeta{GenerateName: "pr-fail-"}},
+ },
+ expectedStatusCount: 0,
+ expectedLogSnippet: "error reporting status check from repo settings",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ observerCore, logCatcher := zapobserver.New(zap.InfoLevel)
+ logger := zap.New(observerCore).Sugar()
+ ctx, _ := rtesting.SetupFakeContext(t)
+ stdata, _ := testclient.SeedTestData(t, ctx, testclient.Data{})
+
+ vcx := &statusCapturingProvider{}
+ vcx.CreateStatusErorring = tt.createStatusErorring
+
+ event := &info.Event{
+ TriggerTarget: triggertype.PullRequest,
+ }
+
+ repo := &v1alpha1.Repository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-repo",
+ Namespace: "test-ns",
+ },
+ Spec: v1alpha1.RepositorySpec{
+ Settings: &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ UnmatchedConclusion: tt.noMatchConclusion,
+ },
+ },
+ },
+ }
+
+ run := ¶ms.Run{
+ Clients: clients.Clients{},
+ }
+ run.Clients.SetConsoleUI(consoleui.FallBackConsole{})
+
+ p := &PacRun{
+ event: event,
+ vcx: vcx,
+ run: run,
+ logger: logger,
+ eventEmitter: events.NewEventEmitter(stdata.Kube, logger),
+ }
+
+ p.reportStatusCheckFromRepoSettings(ctx, repo, tt.unmatchedPRs, "")
+
+ assert.Equal(t, len(vcx.statuses), tt.expectedStatusCount)
+
+ for _, s := range vcx.statuses {
+ assert.Equal(t, s.Status, CompletedStatus)
+ assert.Equal(t, s.Conclusion, tt.expectedConclusion)
+ assert.Assert(t, s.PipelineRun != nil)
+ assert.Assert(t, s.PipelineRunName != "")
+ assert.Equal(t, s.PipelineRunName, s.OriginalPipelineRunName)
+ }
+
+ if tt.name == "uses name over generateName" && len(vcx.statuses) > 0 {
+ assert.Equal(t, vcx.statuses[0].PipelineRunName, "my-named-pr")
+ }
+
+ if tt.expectedLogSnippet != "" {
+ assert.Assert(t, logCatcher.FilterMessageSnippet(tt.expectedLogSnippet).Len() > 0, logCatcher.All())
+ }
+ })
+ }
+}
diff --git a/pkg/pipelineascode/pipelineascode_test.go b/pkg/pipelineascode/pipelineascode_test.go
index cc3657527a..a40055bf98 100644
--- a/pkg/pipelineascode/pipelineascode_test.go
+++ b/pkg/pipelineascode/pipelineascode_test.go
@@ -232,6 +232,30 @@ func TestRun(t *testing.T) {
finalStatus: "neutral",
finalStatusText: "
Status | Duration | Name | ",
},
+ {
+ name: "pull request/allowed with status check mode per unmatched pipelinerun",
+ runevent: info.Event{
+ Event: &github.PullRequestEvent{
+ PullRequest: &github.PullRequest{
+ Number: new(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/no-match",
+ finalStatus: "skipped",
+ finalStatusText: "PipelineRun no-match is not matched to event pull_request",
+ },
{
name: "pull request/with webhook",
runevent: info.Event{
diff --git a/pkg/pipelineascode/testdata/no-match/.tekton/nomatch.yaml b/pkg/pipelineascode/testdata/no-match/.tekton/nomatch.yaml
index 79ae57effc..92e5ec9c8f 100644
--- a/pkg/pipelineascode/testdata/no-match/.tekton/nomatch.yaml
+++ b/pkg/pipelineascode/testdata/no-match/.tekton/nomatch.yaml
@@ -2,6 +2,9 @@ apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: no-match
+ annotations:
+ pipelinesascode.tekton.dev/on-target-branch: "[non-existent-branch]"
+ pipelinesascode.tekton.dev/on-event: "[push]"
spec:
pipelineRef:
name: pipeline1
diff --git a/pkg/provider/bitbucketcloud/bitbucket.go b/pkg/provider/bitbucketcloud/bitbucket.go
index 878dcac658..11a0542f9f 100644
--- a/pkg/provider/bitbucketcloud/bitbucket.go
+++ b/pkg/provider/bitbucketcloud/bitbucket.go
@@ -107,7 +107,7 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusopts
switch statusopts.Conclusion {
case status.ConclusionSkipped:
state = types.StateStopped
- statusopts.Title = "➖ Skipping this commit"
+ statusopts.Title = "➖ Skipping this PipelineRun"
case status.ConclusionNeutral:
state = types.StateStopped
statusopts.Title = "➖ CI has stopped"
diff --git a/pkg/provider/bitbucketcloud/bitbucket_test.go b/pkg/provider/bitbucketcloud/bitbucket_test.go
index 094e29c013..d7790ae9f3 100644
--- a/pkg/provider/bitbucketcloud/bitbucket_test.go
+++ b/pkg/provider/bitbucketcloud/bitbucket_test.go
@@ -343,6 +343,14 @@ func TestCreateStatus(t *testing.T) {
},
expectedDescSubstr: "started",
},
+ {
+ name: "skipped",
+ status: status.StatusOpts{
+ Conclusion: "skipped",
+ OriginalPipelineRunName: originalPipelineRunName,
+ },
+ expectedDescSubstr: "Skipping",
+ },
{
name: "success",
status: status.StatusOpts{
diff --git a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go
index b5612c5149..eb9a7e2d7c 100644
--- a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go
+++ b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go
@@ -97,8 +97,8 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
switch statusOpts.Conclusion {
case status.ConclusionSkipped:
- state = scm.StateFailure
- statusOpts.Title = "➖ Skipping this commit"
+ state = scm.StateUnknown
+ statusOpts.Title = "➖ Skipping this PipelineRun"
case status.ConclusionNeutral:
state = scm.StateFailure
statusOpts.Title = "➖ CI has stopped"
diff --git a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go
index fa5113d0c4..1e3d6e71a9 100644
--- a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go
+++ b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go
@@ -211,6 +211,15 @@ func TestCreateStatus(t *testing.T) {
Text: "Pending approval, waiting for an /ok-to-test",
},
+ pacOpts: pacopts,
+ },
+ {
+ name: "good/skipped",
+ status: status.StatusOpts{
+ Conclusion: "skipped",
+ Text: "Skipping",
+ },
+
pacOpts: pacopts,
},
}
diff --git a/pkg/provider/gitea/gitea.go b/pkg/provider/gitea/gitea.go
index c42c7ad8e0..8d7851ad6e 100644
--- a/pkg/provider/gitea/gitea.go
+++ b/pkg/provider/gitea/gitea.go
@@ -318,7 +318,10 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
case providerstatus.ConclusionNeutral:
statusOpts.Title = "Unknown"
statusOpts.Summary = "doesn't know what happened with this commit."
- case providerstatus.ConclusionCancelled, providerstatus.ConclusionCompleted, providerstatus.ConclusionSkipped:
+ case providerstatus.ConclusionSkipped:
+ statusOpts.Title = "Skipped"
+ statusOpts.Summary = "has skipped."
+ case providerstatus.ConclusionCancelled, providerstatus.ConclusionCompleted:
}
if statusOpts.Status == "in_progress" {
@@ -340,7 +343,9 @@ func (v *Provider) createStatusCommit(ctx context.Context, event *info.Event, pa
state := forgejo.StatusState(status.Conclusion)
switch status.Conclusion {
case providerstatus.ConclusionNeutral:
- state = forgejo.StatusSuccess // We don't have a choice than setting as success, no pending here.c
+ state = forgejo.StatusSuccess // We don't have a choice than setting as success, no pending here.
+ case providerstatus.ConclusionSkipped:
+ state = forgejo.StatusSuccess // We don't have a choice than setting as success, skipped is neither pending nor failure.
case providerstatus.ConclusionPending:
if status.Title != "" {
state = forgejo.StatusPending
@@ -393,7 +398,7 @@ func (v *Provider) createStatusCommit(ctx context.Context, event *info.Event, pa
v.Logger.Warn("Comments related to PipelineRuns status have been disabled for Gitea/Forgejo pull requests")
return nil
case provider.UpdateCommentStrategy:
- if eventType == triggertype.PullRequest || event.TriggerTarget == triggertype.PullRequest {
+ if !status.IsUnmatchedReport && (eventType == triggertype.PullRequest || event.TriggerTarget == triggertype.PullRequest) {
status.Text = strings.ReplaceAll(strings.TrimSpace(status.Text), "
", "\n")
statusComment := v.formatPipelineComment(event.SHA, status)
// Creating the prefix that is added to the status comment for a pipeline run.
diff --git a/pkg/provider/gitea/status_test.go b/pkg/provider/gitea/status_test.go
index 49d82b2668..9d88d8b732 100644
--- a/pkg/provider/gitea/status_test.go
+++ b/pkg/provider/gitea/status_test.go
@@ -163,6 +163,7 @@ func TestProviderCreateStatus(t *testing.T) {
}
func TestProviderCreateStatusCommit(t *testing.T) {
+ commentCreationAPICalled := false
type args struct {
event *info.Event
pacopts *info.PacOpts
@@ -172,6 +173,7 @@ func TestProviderCreateStatusCommit(t *testing.T) {
name string
args args
wantErr bool
+ wantCommentCreationAPICalled bool
wantCommentJSON, wantStatusJSON string
}{
{
@@ -252,8 +254,9 @@ func TestProviderCreateStatusCommit(t *testing.T) {
SHA: "123456",
},
},
- wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
- wantCommentJSON: `{"body":"\ntime to get started"}`,
+ wantCommentCreationAPICalled: true,
+ wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
+ wantCommentJSON: `{"body":"\ntime to get started"}`,
},
{
name: "cancel",
@@ -274,8 +277,9 @@ func TestProviderCreateStatusCommit(t *testing.T) {
SHA: "123456",
},
},
- wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
- wantCommentJSON: `{"body":"\ntime to get started"}`,
+ wantCommentCreationAPICalled: true,
+ wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
+ wantCommentJSON: `{"body":"\ntime to get started"}`,
},
{
name: "retest",
@@ -296,12 +300,57 @@ func TestProviderCreateStatusCommit(t *testing.T) {
SHA: "123456",
},
},
- wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
- wantCommentJSON: `{"body":"\ntime to get started"}`,
+ wantCommentCreationAPICalled: true,
+ wantStatusJSON: `{"state":"pending","target_url":"","description":"Pipeline run for myapp has been triggered","context":"myapp"}`,
+ wantCommentJSON: `{"body":"\ntime to get started"}`,
+ },
+ {
+ name: "skipped",
+ args: args{
+ status: status.StatusOpts{
+ Conclusion: status.ConclusionSkipped,
+ Title: "Skipped",
+ Text: "has skipped.",
+ },
+ pacopts: &info.PacOpts{Settings: settings.Settings{
+ ApplicationName: "myapp",
+ }},
+ event: &info.Event{
+ Organization: "myorg",
+ Repository: "myrepo",
+ PullRequestNumber: 1,
+ TriggerTarget: "pull_request",
+ SHA: "123456",
+ },
+ },
+ wantCommentCreationAPICalled: true,
+ wantStatusJSON: `{"state":"success","target_url":"","description":"Skipped","context":"myapp"}`,
+ wantCommentJSON: `{"body":"\nhas \u003cb\u003eskipped\u003c/b\u003e."}`,
+ },
+ {
+ name: "unmatched report",
+ args: args{
+ pacopts: &info.PacOpts{Settings: settings.Settings{
+ ApplicationName: "myapp",
+ }},
+ event: &info.Event{
+ Organization: "myorg",
+ Repository: "myrepo",
+ PullRequestNumber: 1,
+ TriggerTarget: "pull_request",
+ SHA: "123456",
+ },
+ status: status.StatusOpts{
+ Conclusion: status.ConclusionSkipped,
+ IsUnmatchedReport: true,
+ },
+ },
+ wantStatusJSON: `{"state":"success","target_url":"","description":"","context":"myapp"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ commentCreationAPICalled = false
fakeclient, mux, teardown := tgitea.Setup(t)
defer teardown()
@@ -322,6 +371,7 @@ func TestProviderCreateStatusCommit(t *testing.T) {
// Mock the CreateIssueComment API
mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/issues/%d/comments", tt.args.event.Organization, tt.args.event.Repository, tt.args.event.PullRequestNumber), func(rw http.ResponseWriter, r *http.Request) {
+ commentCreationAPICalled = true
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(rw, "Failed to read request body", http.StatusInternalServerError)
@@ -341,6 +391,7 @@ func TestProviderCreateStatusCommit(t *testing.T) {
if err := v.createStatusCommit(context.Background(), tt.args.event, tt.args.pacopts, tt.args.status); (err != nil) != tt.wantErr {
t.Errorf("Provider.createStatusCommit() error = %v, wantErr %v", err, tt.wantErr)
}
+ assert.Equal(t, tt.wantCommentCreationAPICalled, commentCreationAPICalled)
})
}
}
diff --git a/pkg/provider/github/status.go b/pkg/provider/github/status.go
index 3b182a69c0..b48dbc80d5 100644
--- a/pkg/provider/github/status.go
+++ b/pkg/provider/github/status.go
@@ -202,11 +202,14 @@ func (v *Provider) createCheckRunStatus(ctx context.Context, runevent *info.Even
HeadSHA: runevent.SHA,
Status: new(status.Status), // take status from statusOpts because it can be in_progress, queued, or failure // same for conclusion as well
Output: output,
- DetailsURL: new(status.DetailsURL),
ExternalID: new(status.PipelineRunName),
StartedAt: &now,
}
+ if status.DetailsURL != "" {
+ checkrunoption.DetailsURL = new(status.DetailsURL)
+ }
+
switch {
case conclusion != "":
checkrunoption.Conclusion = new(conclusion)
@@ -393,9 +396,10 @@ func (v *Provider) getOrUpdateCheckRunStatus(ctx context.Context, runevent *info
}
// Patch the pipelineRun with the checkRunID and logURL only when the pipelineRun is not nil and has a name
- // because on validation failed PipelineRun will provide PipelineRun struct but it is not a valid resource
- // created in cluster so if its only validation error report then ignore patching the pipelineRun.
- if statusOpts.PipelineRun != nil && (statusOpts.PipelineRun.GetName() != "" || statusOpts.PipelineRun.GetGenerateName() != "") {
+ // and this is not an unmatched report because on validation failed PipelineRun will provide PipelineRun
+ // struct but it is not a valid resource created in cluster so if its only validation error report then ignore
+ // patching the pipelineRun.
+ if !statusOpts.IsUnmatchedReport && statusOpts.PipelineRun != nil && (statusOpts.PipelineRun.GetName() != "" || statusOpts.PipelineRun.GetGenerateName() != "") {
if _, err := action.PatchPipelineRun(ctx, v.Logger, "checkRunID and logURL", v.Run.Clients.Tekton, statusOpts.PipelineRun, metadataPatch(checkRunID, statusOpts.DetailsURL)); err != nil {
return err
}
@@ -464,6 +468,8 @@ func (v *Provider) createStatusCommit(ctx context.Context, runevent *info.Event,
switch status.Conclusion {
case providerstatus.ConclusionNeutral:
status.Conclusion = providerstatus.ConclusionSuccess // We don't have a choice other than setting as success, no pending here.
+ case providerstatus.ConclusionSkipped:
+ status.Conclusion = providerstatus.ConclusionSuccess // GitHub commit status API doesn't support "skipped", map to success.
case providerstatus.ConclusionPending:
if status.Title != "" {
status.Conclusion = providerstatus.ConclusionPending
@@ -504,7 +510,7 @@ func (v *Provider) createStatusCommit(ctx context.Context, runevent *info.Event,
return nil
case provider.UpdateCommentStrategy:
if (status.Status == "completed" || (status.Status == "queued" && status.Title == pendingApproval)) &&
- status.Text != "" && eventType == triggertype.PullRequest {
+ status.Text != "" && (eventType == triggertype.PullRequest || runevent.TriggerTarget == triggertype.PullRequest) && !status.IsUnmatchedReport {
statusComment := v.formatPipelineComment(runevent.SHA, status)
// Creating the prefix that is added to the status comment for a pipeline run.
plrStatusCommentPrefix := fmt.Sprintf(provider.PlrStatusCommentPrefixTemplate, status.OriginalPipelineRunName)
@@ -523,7 +529,7 @@ func (v *Provider) createStatusCommit(ctx context.Context, runevent *info.Event,
}
default:
if (status.Status == "completed" || (status.Status == "queued" && status.Title == pendingApproval)) &&
- status.Text != "" && eventType == triggertype.PullRequest {
+ status.Text != "" && (eventType == triggertype.PullRequest || runevent.TriggerTarget == triggertype.PullRequest) && !status.IsUnmatchedReport {
_, _, err = wrapAPI(v, "create_issue_comment", func() (*github.IssueComment, *github.Response, error) {
return v.Client().Issues.CreateComment(
ctx, runevent.Organization, runevent.Repository,
@@ -578,7 +584,10 @@ func (v *Provider) CreateStatus(ctx context.Context, runevent *info.Event, statu
statusOpts.Title = "Unknown"
}
statusOpts.Summary = "Completed"
- case providerstatus.ConclusionCompleted, providerstatus.ConclusionSkipped:
+ case providerstatus.ConclusionSkipped:
+ statusOpts.Title = "Skipped"
+ statusOpts.Summary = "has skipped this PipelineRun."
+ case providerstatus.ConclusionCompleted:
}
if statusOpts.Status == "in_progress" {
diff --git a/pkg/provider/github/status_test.go b/pkg/provider/github/status_test.go
index b811d8ea6f..97b07339dd 100644
--- a/pkg/provider/github/status_test.go
+++ b/pkg/provider/github/status_test.go
@@ -17,6 +17,7 @@ import (
"github.com/google/go-github/v91/github"
"github.com/jonboulle/clockwork"
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
+ pipelinesascode "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"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"
@@ -28,82 +29,145 @@ import (
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"gotest.tools/v3/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ k8stesting "k8s.io/client-go/testing"
rtesting "knative.dev/pkg/reconciler/testing"
)
-func TestGithubProviderCreateCheckRun(t *testing.T) {
- ctx, _ := rtesting.SetupFakeContext(t)
- fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
- l, _ := logger.GetLogger()
- cnx := Provider{
- ghClient: fakeclient,
- Run: params.New(),
- pacInfo: &info.PacOpts{
- Settings: settings.Settings{
- ApplicationName: settings.PACApplicationNameDefaultValue,
+func TestGetOrUpdateCheckRunStatus(t *testing.T) {
+ tests := []struct {
+ name string
+ statusOpts []providerstatus.StatusOpts
+ expectPatched bool
+ }{
+ {
+ name: "create check run with pipeline run name",
+ expectPatched: true,
+ statusOpts: []providerstatus.StatusOpts{
+ {
+ PipelineRunName: "pr1",
+ Status: "hello moto",
+ PipelineRun: &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pr1",
+ Namespace: "default",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "multiple failed PipelineRuns only creates one check run",
+ expectPatched: true,
+ statusOpts: []providerstatus.StatusOpts{
+ {
+ PipelineRunName: "",
+ Title: "Failed",
+ InstanceCountForCheckRun: 0,
+ PipelineRun: &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "failed-pr",
+ Namespace: "default",
+ },
+ },
+ },
+ {
+ PipelineRunName: "",
+ Title: "Failed",
+ InstanceCountForCheckRun: 1,
+ },
+ },
+ },
+ {
+ name: "matched report patches PipelineRun",
+ expectPatched: true,
+ statusOpts: []providerstatus.StatusOpts{
+ {
+ PipelineRunName: "matched-pr",
+ Status: "completed",
+ Conclusion: providerstatus.ConclusionSuccess,
+ Text: "PipelineRun matched",
+ PipelineRun: &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "matched-pr",
+ Namespace: "default",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "unmatched report skips PipelineRun patch",
+ statusOpts: []providerstatus.StatusOpts{
+ {
+ PipelineRunName: "unmatched-pr",
+ Status: "completed",
+ Conclusion: providerstatus.ConclusionSkipped,
+ Text: "PipelineRun not matched",
+ IsUnmatchedReport: true,
+ PipelineRun: &tektonv1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "unmatched-pr",
+ Namespace: "default",
+ },
+ },
+ },
},
},
- Logger: l,
}
- defer teardown()
- mux.HandleFunc("/repos/check/info/check-runs", func(w http.ResponseWriter, _ *http.Request) {
- _, _ = fmt.Fprint(w, `{"id": 555}`)
- })
-
- mux.HandleFunc("/repos/check/info/check-runs/555", func(w http.ResponseWriter, _ *http.Request) {
- _, _ = fmt.Fprint(w, `{"id": 555}`)
- })
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, _ := rtesting.SetupFakeContext(t)
+ fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
+ defer teardown()
- event := &info.Event{
- Organization: "check",
- Repository: "info",
- SHA: "createCheckRunSHA",
- }
+ l, _ := logger.GetLogger()
+ run := params.New()
+ testData := testclient.Data{}
+ for _, so := range tt.statusOpts {
+ if so.PipelineRun != nil {
+ testData.PipelineRuns = append(testData.PipelineRuns, so.PipelineRun)
+ }
+ }
+ stdata, _ := testclient.SeedTestData(t, ctx, testData)
+ run.Clients.Tekton = stdata.Pipeline
- err := cnx.getOrUpdateCheckRunStatus(ctx, event, providerstatus.StatusOpts{
- PipelineRunName: "pr1",
- Status: "hello moto",
- })
- assert.NilError(t, err)
-}
+ var patched bool
+ stdata.Pipeline.PrependReactor("patch", "pipelineruns", func(_ k8stesting.Action) (bool, runtime.Object, error) {
+ patched = true
+ return false, nil, nil
+ })
-func TestGetOrUpdateCheckRunStatusForMultipleFailedPipelineRun(t *testing.T) {
- ctx, _ := rtesting.SetupFakeContext(t)
- fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
- l, _ := logger.GetLogger()
- cnx := Provider{
- ghClient: fakeclient,
- Run: params.New(),
- pacInfo: &info.PacOpts{},
- Logger: l,
- }
- defer teardown()
- statusOptionData := []providerstatus.StatusOpts{{
- PipelineRunName: "",
- Title: "Failed",
- InstanceCountForCheckRun: 0,
- }, {
- PipelineRunName: "",
- Title: "Failed",
- InstanceCountForCheckRun: 1,
- }}
- mux.HandleFunc("/repos/check/info/check-runs", func(w http.ResponseWriter, _ *http.Request) {
- _, _ = fmt.Fprint(w, `{"id": 555}`)
- })
+ cnx := Provider{
+ ghClient: fakeclient,
+ Run: run,
+ pacInfo: &info.PacOpts{
+ Settings: settings.Settings{
+ ApplicationName: settings.PACApplicationNameDefaultValue,
+ },
+ },
+ Logger: l,
+ }
- mux.HandleFunc("/repos/check/info/check-runs/555", func(w http.ResponseWriter, _ *http.Request) {
- _, _ = fmt.Fprint(w, `{"id": 555}`)
- })
+ mux.HandleFunc("/repos/check/info/check-runs", func(w http.ResponseWriter, _ *http.Request) {
+ _, _ = fmt.Fprint(w, `{"id": 555}`)
+ })
+ mux.HandleFunc("/repos/check/info/check-runs/555", func(w http.ResponseWriter, _ *http.Request) {
+ _, _ = fmt.Fprint(w, `{"id": 555}`)
+ })
- event := &info.Event{
- Organization: "check",
- Repository: "info",
- SHA: "createCheckRunSHA",
- }
+ event := &info.Event{
+ Organization: "check",
+ Repository: "info",
+ SHA: "createCheckRunSHA",
+ }
- for i := range statusOptionData {
- err := cnx.getOrUpdateCheckRunStatus(ctx, event, statusOptionData[i])
- assert.NilError(t, err)
+ for i := range tt.statusOpts {
+ err := cnx.getOrUpdateCheckRunStatus(ctx, event, tt.statusOpts[i])
+ assert.NilError(t, err)
+ }
+ assert.Equal(t, patched, tt.expectPatched)
+ })
}
}
@@ -409,6 +473,34 @@ func TestGithubProviderCreateStatus(t *testing.T) {
want: &github.CheckRun{ID: &resultid},
wantErr: false,
},
+ {
+ name: "skipped via github apps",
+ args: args{
+ runevent: runEvent,
+ status: "completed",
+ conclusion: "skipped",
+ text: "PipelineRun not matched",
+ detailsURL: "https://cireport.com",
+ titleSubstr: "Skipped",
+ githubApps: true,
+ },
+ want: &github.CheckRun{ID: &resultid},
+ wantErr: false,
+ },
+ {
+ name: "skipped via webhook",
+ args: args{
+ runevent: runEvent,
+ status: "completed",
+ conclusion: "skipped",
+ text: "PipelineRun not matched",
+ detailsURL: "https://cireport.com",
+ titleSubstr: "Skipped",
+ githubApps: false,
+ },
+ want: &github.CheckRun{ID: &resultid},
+ wantErr: false,
+ },
{
name: "unknown",
args: args{
@@ -549,6 +641,7 @@ func TestGithubProviderCreateStatus(t *testing.T) {
}
func TestGithubProvidercreateStatusCommit(t *testing.T) {
+ commentCreationAPICalled := false
issuenumber := 666
anevent := &info.Event{
Event: &github.PullRequestEvent{PullRequest: &github.PullRequest{Number: new(issuenumber)}},
@@ -559,11 +652,13 @@ func TestGithubProvidercreateStatusCommit(t *testing.T) {
PullRequestNumber: issuenumber,
}
tests := []struct {
- name string
- event *info.Event
- wantErr bool
- status providerstatus.StatusOpts
- expectedConclusion string
+ name string
+ event *info.Event
+ repo *pipelinesascode.Repository
+ wantErr bool
+ wantCommentCreationAPICalled bool
+ status providerstatus.StatusOpts
+ expectedConclusion string
}{
{
name: "completed",
@@ -574,7 +669,8 @@ func TestGithubProvidercreateStatusCommit(t *testing.T) {
Text: "Finito amigo",
Conclusion: "completed",
},
- expectedConclusion: "completed",
+ expectedConclusion: "completed",
+ wantCommentCreationAPICalled: true,
},
{
name: "in_progress",
@@ -600,9 +696,37 @@ func TestGithubProvidercreateStatusCommit(t *testing.T) {
},
expectedConclusion: "success",
},
+ {
+ name: "pull_request status skipped",
+ event: anevent,
+ status: providerstatus.StatusOpts{
+ Conclusion: providerstatus.ConclusionSkipped,
+ },
+ expectedConclusion: "success",
+ },
+ {
+ name: "unmatched report",
+ event: anevent,
+ repo: &pipelinesascode.Repository{
+ Spec: pipelinesascode.RepositorySpec{
+ Settings: &pipelinesascode.Settings{
+ Github: &pipelinesascode.GithubSettings{
+ CommentStrategy: "", // keep it empty so that comment strategy will be default and try to create a comment
+ },
+ },
+ },
+ },
+ status: providerstatus.StatusOpts{
+ Conclusion: providerstatus.ConclusionSkipped,
+ IsUnmatchedReport: true,
+ },
+ expectedConclusion: "success",
+ wantCommentCreationAPICalled: false,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ commentCreationAPICalled = false
fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
defer teardown()
mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/statuses/%s",
@@ -613,6 +737,7 @@ func TestGithubProvidercreateStatusCommit(t *testing.T) {
if tt.status.Status == "completed" {
mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/issues/%d/comments",
tt.event.Organization, tt.event.Repository, issuenumber), func(_ http.ResponseWriter, r *http.Request) {
+ commentCreationAPICalled = true
body, _ := io.ReadAll(r.Body)
assert.Equal(t, fmt.Sprintf(`{"body":"%s
%s"}`, tt.status.Summary, tt.status.Text)+"\n", string(body))
})
@@ -631,9 +756,14 @@ func TestGithubProvidercreateStatusCommit(t *testing.T) {
Logger: l,
}
+ if tt.repo != nil {
+ provider.repo = tt.repo
+ }
+
if err := provider.createStatusCommit(ctx, tt.event, tt.status); (err != nil) != tt.wantErr {
t.Errorf("GetCommitInfo() error = %v, wantErr %v", err, tt.wantErr)
}
+ assert.Equal(t, tt.wantCommentCreationAPICalled, commentCreationAPICalled)
})
}
}
diff --git a/pkg/provider/gitlab/gitlab.go b/pkg/provider/gitlab/gitlab.go
index 298e33cc66..532dfb64b7 100644
--- a/pkg/provider/gitlab/gitlab.go
+++ b/pkg/provider/gitlab/gitlab.go
@@ -634,7 +634,7 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
v.Logger.Warn("Comments related to PipelineRuns status have been disabled for GitLab merge requests")
return nil
case provider.UpdateCommentStrategy:
- if eventType == triggertype.PullRequest || provider.Valid(event.EventType, anyMergeRequestEventType) {
+ if (eventType == triggertype.PullRequest || provider.Valid(event.EventType, anyMergeRequestEventType)) && !statusOpts.IsUnmatchedReport {
statusComment := v.formatPipelineComment(event.SHA, statusOpts)
// Creating the prefix that is added to the status comment for a pipeline run.
plrStatusCommentPrefix := fmt.Sprintf(provider.PlrStatusCommentPrefixTemplate, statusOpts.OriginalPipelineRunName)
@@ -652,7 +652,7 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
}
}
default:
- if eventType == triggertype.PullRequest || provider.Valid(event.EventType, anyMergeRequestEventType) {
+ if (eventType == triggertype.PullRequest || provider.Valid(event.EventType, anyMergeRequestEventType)) && !statusOpts.IsUnmatchedReport {
mopt := &gitlab.CreateMergeRequestNoteOptions{Body: gitlab.Ptr(body)}
_, _, err := v.Client().Notes.CreateMergeRequestNote(event.TargetProjectID, int64(event.PullRequestNumber), mopt)
return err
@@ -1142,6 +1142,9 @@ func (v *Provider) storePipelineID(ctx context.Context, statusOpts providerstatu
// patchPipelineIDAnnotation stores the GitLab pipeline ID as a PipelineRun
// annotation so the reconciler can read it back across Provider instances.
func (v *Provider) patchPipelineIDAnnotation(ctx context.Context, statusOpts providerstatus.StatusOpts, pipelineID int64) {
+ if statusOpts.IsUnmatchedReport {
+ return
+ }
pr := statusOpts.PipelineRun
if pr == nil || (pr.GetName() == "" && pr.GetGenerateName() == "") {
return
diff --git a/pkg/provider/gitlab/gitlab_test.go b/pkg/provider/gitlab/gitlab_test.go
index a7738bc8cd..3df307b4db 100644
--- a/pkg/provider/gitlab/gitlab_test.go
+++ b/pkg/provider/gitlab/gitlab_test.go
@@ -701,6 +701,122 @@ func TestCreateStatus(t *testing.T) {
}
}
+func TestCreateStatusUnmatchedReportSkipsMRComment(t *testing.T) {
+ tests := []struct {
+ name string
+ isUnmatchedReport bool
+ repo *v1alpha1.Repository
+ wantNoteCreated bool
+ }{
+ {
+ name: "unmatched report skips MR comment with default comment strategy",
+ isUnmatchedReport: true,
+ wantNoteCreated: false,
+ },
+ {
+ name: "matched report creates MR comment with default comment strategy",
+ isUnmatchedReport: false,
+ wantNoteCreated: true,
+ },
+ {
+ name: "unmatched report skips MR comment with update comment strategy",
+ isUnmatchedReport: true,
+ repo: &v1alpha1.Repository{
+ Spec: v1alpha1.RepositorySpec{
+ Settings: &v1alpha1.Settings{
+ Gitlab: &v1alpha1.GitlabSettings{
+ CommentStrategy: "update",
+ },
+ },
+ },
+ },
+ wantNoteCreated: false,
+ },
+ {
+ name: "matched report creates MR comment with update comment strategy",
+ isUnmatchedReport: false,
+ repo: &v1alpha1.Repository{
+ Spec: v1alpha1.RepositorySpec{
+ Settings: &v1alpha1.Settings{
+ Gitlab: &v1alpha1.GitlabSettings{
+ CommentStrategy: "update",
+ },
+ },
+ },
+ },
+ wantNoteCreated: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, _ := rtesting.SetupFakeContext(t)
+ log, _ := logger.GetLogger()
+ stdata, _ := testclient.SeedTestData(t, ctx, testclient.Data{})
+ run := ¶ms.Run{
+ Clients: clients.Clients{
+ Kube: stdata.Kube,
+ Tekton: stdata.Pipeline,
+ Log: log,
+ },
+ }
+
+ client, mux, tearDown := thelp.Setup(t)
+ defer tearDown()
+
+ v := &Provider{
+ targetProjectID: 100,
+ run: run,
+ Logger: log,
+ repo: tt.repo,
+ pacInfo: &info.PacOpts{
+ Settings: settings.Settings{
+ ApplicationName: settings.PACApplicationNameDefaultValue,
+ },
+ },
+ eventEmitter: events.NewEventEmitter(run.Clients.Kube, log),
+ }
+ v.SetGitLabClient(client)
+
+ event := &info.Event{
+ TriggerTarget: "pull_request",
+ EventType: "Merge Request",
+ SourceProjectID: 400,
+ TargetProjectID: 400,
+ SHA: "abc123",
+ PullRequestNumber: 42,
+ }
+
+ mux.HandleFunc("/user", func(rw http.ResponseWriter, _ *http.Request) {
+ fmt.Fprint(rw, `{"id": 100}`)
+ })
+
+ // Both source and target return errors so CreateStatus falls through to MR comment path
+ mux.HandleFunc("/projects/400/statuses/abc123", func(rw http.ResponseWriter, _ *http.Request) {
+ rw.WriteHeader(http.StatusBadRequest)
+ fmt.Fprint(rw, `{"message": "400 Bad Request"}`)
+ })
+
+ noteCreated := false
+ mux.HandleFunc(fmt.Sprintf("/projects/%d/merge_requests/42/notes", event.TargetProjectID), func(rw http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet {
+ fmt.Fprint(rw, `[]`)
+ return
+ }
+ noteCreated = true
+ fmt.Fprint(rw, `{}`)
+ })
+
+ err := v.CreateStatus(ctx, event, providerstatus.StatusOpts{
+ Conclusion: providerstatus.ConclusionSkipped,
+ OriginalPipelineRunName: "test-pr",
+ IsUnmatchedReport: tt.isUnmatchedReport,
+ })
+ assert.NilError(t, err)
+ assert.Equal(t, tt.wantNoteCreated, noteCreated, "MR note creation mismatch")
+ })
+ }
+}
+
func TestCreateStatusPipelineIDSharedAcrossPipelineRuns(t *testing.T) {
ctx, _ := rtesting.SetupFakeContext(t)
log, _ := logger.GetLogger()
diff --git a/pkg/provider/status/status.go b/pkg/provider/status/status.go
index a6cf21a401..0bd4fa6f24 100644
--- a/pkg/provider/status/status.go
+++ b/pkg/provider/status/status.go
@@ -34,4 +34,5 @@ type StatusOpts struct {
Title string
InstanceCountForCheckRun int
AccessDenied bool
+ IsUnmatchedReport bool // if true, the status is reported for unmatched pipelineruns
}
diff --git a/test/bitbucket_cloud_pullrequest_test.go b/test/bitbucket_cloud_pullrequest_test.go
index cbec9513b4..67e0cd52dd 100644
--- a/test/bitbucket_cloud_pullrequest_test.go
+++ b/test/bitbucket_cloud_pullrequest_test.go
@@ -11,6 +11,7 @@ import (
"github.com/ktrysmt/go-bitbucket"
"github.com/mitchellh/mapstructure"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud/types"
tbb "github.com/openshift-pipelines/pipelines-as-code/test/pkg/bitbucketcloud"
@@ -208,6 +209,80 @@ func TestBitbucketCloudPRBuildStatusReported(t *testing.T) {
assert.Equal(t, foundStatus, true, "should have found the status for the pipeline run")
}
+func TestBitbucketCloudPRSkippedStatusReported(t *testing.T) {
+ targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
+ ctx := context.Background()
+
+ runcnx, opts, bprovider, err := tbb.Setup(ctx)
+ if err != nil {
+ t.Skip(err.Error())
+ return
+ }
+ opts.Settings = &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ }
+ bcrepo := tbb.CreateCRD(ctx, t, bprovider, runcnx, opts, targetNS)
+ targetRefName := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-test")
+ title := "TestPullRequest - " + targetRefName
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ pr, repobranch := tbb.MakePR(t, bprovider, runcnx, bcrepo, opts, title, targetRefName, entries)
+ defer tbb.TearDown(ctx, t, runcnx, bprovider, opts, pr.ID, targetRefName, targetNS, false)
+
+ hash, ok := repobranch.Target["hash"].(string)
+ assert.Assert(t, ok)
+
+ sopt := twait.SuccessOpt{
+ TargetNS: targetNS,
+ OnEvent: triggertype.PullRequest.String(),
+ NumberofPRMatch: 1,
+ SHA: hash,
+ Title: title,
+ MinNumberStatus: 1,
+ }
+ twait.Succeeded(ctx, t, runcnx, opts, sopt)
+
+ resp, err := bprovider.Client().Repositories.Commits.GetCommitStatuses(&bitbucket.CommitsOptions{
+ Owner: opts.Organization,
+ RepoSlug: opts.Repo,
+ Revision: hash,
+ })
+ assert.NilError(t, err)
+
+ statusesMap, ok := resp.(map[string]any)
+ assert.Equal(t, ok, true, "cannot convert Bitbucket commit statuses response to map[string]any")
+
+ statuses := []*types.Status{}
+
+ err = mapstructure.Decode(statusesMap["values"], &statuses)
+ assert.NilError(t, err, fmt.Sprintf("cannot decode Bitbucket commit statuses from response payload: %v", err))
+
+ foundStatus := false
+ for _, status := range statuses {
+ if status.State == "STOPPED" && strings.Contains(status.Description, "Skipping this PipelineRun") {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found the status for the pipeline run")
+}
+
// Local Variables:
// compile-command: "go test -tags=e2e -v -run TestBitbucketCloudPullRequest$ ."
// End:
diff --git a/test/bitbucket_datacenter_dynamic_variables_test.go b/test/bitbucket_datacenter_dynamic_variables_test.go
index 969af2d534..7692ad91ed 100644
--- a/test/bitbucket_datacenter_dynamic_variables_test.go
+++ b/test/bitbucket_datacenter_dynamic_variables_test.go
@@ -27,7 +27,7 @@ func TestBitbucketDataCenterDynamicVariables(t *testing.T) {
ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
assert.NilError(t, err)
- repo := tbbdc.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
diff --git a/test/bitbucket_datacenter_on_comment_test.go b/test/bitbucket_datacenter_on_comment_test.go
index d53614d0da..fe48a232d6 100644
--- a/test/bitbucket_datacenter_on_comment_test.go
+++ b/test/bitbucket_datacenter_on_comment_test.go
@@ -29,7 +29,7 @@ func TestBitbucketDataCenterNonGitopsCommentTriggersPipelineRun(t *testing.T) {
ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
assert.NilError(t, err)
- repo := tbbdc.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
diff --git a/test/bitbucket_datacenter_pull_request_test.go b/test/bitbucket_datacenter_pull_request_test.go
index 56a9670267..b5d66a3b75 100644
--- a/test/bitbucket_datacenter_pull_request_test.go
+++ b/test/bitbucket_datacenter_pull_request_test.go
@@ -6,9 +6,11 @@ import (
"context"
"fmt"
"os"
+ "strings"
"testing"
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
tbbdc "github.com/openshift-pipelines/pipelines-as-code/test/pkg/bitbucketdatacenter"
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/options"
@@ -29,7 +31,7 @@ func TestBitbucketDataCenterPullRequest(t *testing.T) {
ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
assert.NilError(t, err)
- repo := tbbdc.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
@@ -63,7 +65,7 @@ func TestBitbucketDataCenterCELPathChangeInPullRequest(t *testing.T) {
ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
assert.NilError(t, err)
- repo := tbbdc.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
@@ -101,7 +103,7 @@ func TestBitbucketDataCenterOnPathChangeAnnotationOnPRMerge(t *testing.T) {
ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
assert.NilError(t, err)
- repo := tbbdc.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
@@ -147,3 +149,60 @@ func TestBitbucketDataCenterOnPathChangeAnnotationOnPRMerge(t *testing.T) {
// check that pipeline run contains on-path-change annotation.
assert.Equal(t, pipelineRuns.Items[0].GetAnnotations()[keys.OnPathChange], "[doc/***.md]")
}
+
+func TestBitbucketDataCenterPRSkippedStatusReported(t *testing.T) {
+ targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
+ ctx := context.Background()
+ bitbucketWSOwner := os.Getenv("TEST_BITBUCKET_DATA_CENTER_E2E_REPOSITORY")
+
+ ctx, runcnx, opts, client, err := tbbdc.Setup(ctx)
+ assert.NilError(t, err)
+
+ opts.Settings = &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ }
+ repo := tbbdc.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
+ runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
+ defer tbbdc.TearDownNs(ctx, t, runcnx, targetNS)
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ pr := tbbdc.CreatePR(ctx, t, client, runcnx, opts, repo, entries, bitbucketWSOwner, targetNS)
+ runcnx.Clients.Log.Infof("Pull Request with title '%s' is created", pr.Title)
+ defer tbbdc.TearDown(ctx, t, runcnx, client, pr, bitbucketWSOwner, targetNS)
+
+ successOpts := wait.SuccessOpt{
+ TargetNS: targetNS,
+ OnEvent: triggertype.PullRequest.String(),
+ NumberofPRMatch: 1,
+ MinNumberStatus: 1,
+ }
+ wait.Succeeded(ctx, t, runcnx, opts, successOpts)
+
+ statuses, _, err := client.Repositories.ListStatus(ctx, bitbucketWSOwner, pr.Sha, &scm.ListOptions{})
+ assert.NilError(t, err)
+
+ foundStatus := false
+ for _, status := range statuses {
+ if status.State == scm.StateUnknown && strings.Contains(status.Label, "pipelinerun-skipped") {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found the status for the skipped pipeline run")
+}
diff --git a/test/bitbucket_datacenter_push_test.go b/test/bitbucket_datacenter_push_test.go
index 999528fc04..9b2b46bd61 100644
--- a/test/bitbucket_datacenter_push_test.go
+++ b/test/bitbucket_datacenter_push_test.go
@@ -26,7 +26,7 @@ func TestBitbucketDataCenterCELPathChangeOnPush(t *testing.T) {
ctx, runcnx, opts, client, err := tbbs.Setup(ctx)
assert.NilError(t, err)
- repo := tbbs.CreateCRD(ctx, t, client, runcnx, bitbucketWSOwner, targetNS)
+ repo := tbbs.CreateCRD(ctx, t, client, runcnx, opts, bitbucketWSOwner, targetNS)
runcnx.Clients.Log.Infof("Repository %s has been created", repo.Name)
defer tbbs.TearDownNs(ctx, t, runcnx, targetNS)
diff --git a/test/gitea_pull_request_test.go b/test/gitea_pull_request_test.go
index 320daf3dc4..8ea7b4c5fb 100644
--- a/test/gitea_pull_request_test.go
+++ b/test/gitea_pull_request_test.go
@@ -14,6 +14,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
"github.com/openshift-pipelines/pipelines-as-code/pkg/sort"
tgitea "github.com/openshift-pipelines/pipelines-as-code/test/pkg/gitea"
@@ -151,3 +152,71 @@ func TestGiteaPullRequestPrivateRepository(t *testing.T) {
assert.NilError(t, err)
tgitea.WaitForSecretDeletion(t, topts, topts.TargetRefName)
}
+
+func TestGiteaPRSkippedStatusReported(t *testing.T) {
+ topts := &tgitea.TestOpts{
+ TargetEvent: triggertype.PullRequest.String(),
+ NoPullRequestCreation: true,
+ SkipEventsCheck: true,
+ Settings: &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ },
+ }
+ ctx, f := tgitea.TestPR(t, topts)
+ defer f()
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ topts.TargetNS, topts.DefaultBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ topts.TargetNS, topts.DefaultBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ scmOpts := &scm.Opts{
+ GitURL: topts.GitCloneURL,
+ Log: topts.ParamsRun.Clients.Log,
+ WebURL: topts.GitHTMLURL,
+ TargetRefName: topts.TargetRefName,
+ BaseRefName: topts.DefaultBranch,
+ }
+ topts.SHA = scm.PushFilesToRefGit(t, scmOpts, entries)
+
+ pr, _, err := topts.GiteaCNX.Client().CreatePullRequest(topts.Opts.Organization, topts.Opts.Repo, forgejo.CreatePullRequestOption{
+ Title: "Test Pull Request - " + topts.TargetRefName,
+ Head: topts.TargetRefName,
+ Base: topts.DefaultBranch,
+ })
+ assert.NilError(t, err)
+ topts.PullRequest = pr
+ topts.ParamsRun.Clients.Log.Infof("PullRequest %s has been created", pr.HTMLURL)
+
+ sopt := twait.SuccessOpt{
+ TargetNS: topts.TargetNS,
+ OnEvent: triggertype.PullRequest.String(),
+ NumberofPRMatch: 1,
+ MinNumberStatus: 1,
+ }
+ twait.Succeeded(ctx, t, topts.ParamsRun, topts.Opts, sopt)
+
+ statuses, _, err := topts.GiteaCNX.Client().ListStatuses(topts.Opts.Organization, topts.Opts.Repo, topts.SHA, forgejo.ListStatusesOption{})
+ assert.NilError(t, err)
+
+ foundStatus := false
+ for _, cstatus := range statuses {
+ if cstatus.State == forgejo.StatusSuccess && cstatus.Description == "Skipped" {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found the skipped status for the non-matching pipeline run")
+}
diff --git a/test/github_pullrequest_test.go b/test/github_pullrequest_test.go
index 70e5e253fe..d656fd404b 100644
--- a/test/github_pullrequest_test.go
+++ b/test/github_pullrequest_test.go
@@ -5,6 +5,7 @@ package test
import (
"context"
"fmt"
+ "os"
"regexp"
"strings"
"testing"
@@ -20,6 +21,7 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/configmap"
tgithub "github.com/openshift-pipelines/pipelines-as-code/test/pkg/github"
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/options"
+ "github.com/openshift-pipelines/pipelines-as-code/test/pkg/payload"
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
"github.com/google/go-github/v91/github"
@@ -807,6 +809,192 @@ func TestGithubGHEPullRequestEventTimeLogged(t *testing.T) {
assert.NilError(t, err)
}
+func TestGithubGHEPRSkippedStatusReported(t *testing.T) {
+ ctx := context.Background()
+ g := &tgithub.PRTest{
+ Label: "Github Skipped Status",
+ GHE: true,
+ }
+ targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
+ targetRefName := fmt.Sprintf("refs/heads/%s", targetNS)
+
+ ctx, runcnx, opts, ghcnx, err := tgithub.Setup(ctx, true, false)
+ assert.NilError(t, err)
+ g.Cnx = runcnx
+ g.Options = opts
+ g.Provider = ghcnx
+ g.TargetNamespace = targetNS
+ g.Logger = runcnx.Clients.Log
+
+ repoinfo, _, err := ghcnx.Client().Repositories.Get(ctx, opts.Organization, opts.Repo)
+ assert.NilError(t, err)
+
+ opts.Settings = &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ }
+ err = tgithub.CreateCRD(ctx, t, repoinfo, runcnx, opts, ghcnx, targetNS)
+ assert.NilError(t, err)
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ commitTitle := fmt.Sprintf("Testing skipped status on %s", targetNS)
+ g.CommitTitle = commitTitle
+ g.TargetRefName = targetRefName
+
+ sha, _, err := tgithub.PushFilesToRef(ctx, ghcnx.Client(), commitTitle,
+ repoinfo.GetDefaultBranch(), targetRefName, opts.Organization, opts.Repo, entries)
+ assert.NilError(t, err)
+ g.SHA = sha
+
+ number, err := tgithub.PRCreate(ctx, runcnx, ghcnx, opts.Organization,
+ opts.Repo, targetRefName, repoinfo.GetDefaultBranch(), commitTitle)
+ assert.NilError(t, err)
+ g.PRNumber = number
+ defer g.TearDown(ctx, t)
+
+ sopt := twait.SuccessOpt{
+ Title: commitTitle,
+ OnEvent: triggertype.PullRequest.String(),
+ TargetNS: targetNS,
+ NumberofPRMatch: 1,
+ SHA: sha,
+ }
+ twait.Succeeded(ctx, t, runcnx, opts, sopt)
+
+ opt := github.ListOptions{}
+ res := &github.ListCheckRunsResults{}
+ resp := &github.Response{}
+ counter := 0
+ for {
+ res, resp, err = ghcnx.Client().Checks.ListCheckRunsForRef(ctx, opts.Organization, opts.Repo, sha, &github.ListCheckRunsOptions{
+ AppID: ghcnx.ApplicationID,
+ ListOptions: opt,
+ })
+ assert.NilError(t, err)
+ assert.Equal(t, resp.StatusCode, 200)
+ if len(res.CheckRuns) >= 2 {
+ break
+ }
+ runcnx.Clients.Log.Infof("Waiting for the check runs to be created (%d/2)", len(res.CheckRuns))
+ if counter > 20 {
+ t.Fatalf("Check runs not created after 20 tries, got %d", len(res.CheckRuns))
+ }
+ time.Sleep(5 * time.Second)
+ counter++
+ }
+
+ foundStatus := false
+ for _, cr := range res.CheckRuns {
+ if cr.GetConclusion() == "skipped" && strings.Contains(cr.GetName(), "pipelinerun-skipped") {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found a check run with skipped conclusion for the non-matching pipeline run")
+}
+
+func TestGithubGHEWebhookPRSkippedStatusReported(t *testing.T) {
+ ctx := context.Background()
+ g := &tgithub.PRTest{
+ Label: "Github Webhook Skipped Status",
+ GHE: true,
+ Webhook: true,
+ }
+ targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
+ targetRefName := fmt.Sprintf("refs/heads/%s", targetNS)
+
+ ctx, runcnx, opts, ghcnx, err := tgithub.Setup(ctx, true, true)
+ assert.NilError(t, err)
+ g.Cnx = runcnx
+ g.Provider = ghcnx
+ g.TargetNamespace = targetNS
+ g.Logger = runcnx.Clients.Log
+
+ repoName := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-test")
+ smeeURL := os.Getenv("TEST_GITHUB_SECOND_WEBHOOK_SMEE_URL")
+ webhookSecret := os.Getenv("TEST_EL_WEBHOOK_SECRET")
+
+ repoinfo, err := tgithub.CreateGHERepo(ctx, ghcnx.Client(), opts.Organization, repoName, smeeURL, webhookSecret, runcnx.Clients.Log)
+ assert.NilError(t, err)
+ opts.Repo = repoName
+ opts.Settings = &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ }
+ g.Options = opts
+ g.DynamicRepoName = repoName
+
+ err = tgithub.CreateCRD(ctx, t, repoinfo, runcnx, opts, ghcnx, targetNS)
+ assert.NilError(t, err)
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ targetNS, options.MainBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ commitTitle := fmt.Sprintf("Testing webhook skipped status on %s", targetNS)
+ g.CommitTitle = commitTitle
+ g.TargetRefName = targetRefName
+
+ sha, _, err := tgithub.PushFilesToRef(ctx, ghcnx.Client(), commitTitle,
+ repoinfo.GetDefaultBranch(), targetRefName, opts.Organization, opts.Repo, entries)
+ assert.NilError(t, err)
+ g.SHA = sha
+
+ number, err := tgithub.PRCreate(ctx, runcnx, ghcnx, opts.Organization,
+ opts.Repo, targetRefName, repoinfo.GetDefaultBranch(), commitTitle)
+ assert.NilError(t, err)
+ g.PRNumber = number
+ defer g.TearDown(ctx, t)
+
+ sopt := twait.SuccessOpt{
+ Title: commitTitle,
+ OnEvent: triggertype.PullRequest.String(),
+ TargetNS: targetNS,
+ NumberofPRMatch: 1,
+ SHA: sha,
+ }
+ twait.Succeeded(ctx, t, runcnx, opts, sopt)
+
+ statuses, _, err := ghcnx.Client().Repositories.ListStatuses(ctx, opts.Organization, opts.Repo, sha, &github.ListOptions{})
+ assert.NilError(t, err)
+
+ foundStatus := false
+ for _, status := range statuses {
+ if status.GetState() == "success" && status.GetDescription() == "Skipped" && strings.Contains(status.GetContext(), "pipelinerun-skipped") {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found a commit status with success state and Skipped description for the non-matching pipeline run")
+}
+
// Local Variables:
// compile-command: "go test -tags=e2e -v -info TestGithubPullRequest$ ."
// End:
diff --git a/test/gitlab_merge_request_test.go b/test/gitlab_merge_request_test.go
index b304509a7b..5f6941a0e5 100644
--- a/test/gitlab_merge_request_test.go
+++ b/test/gitlab_merge_request_test.go
@@ -901,6 +901,73 @@ func TestGitlabMergeRequestCommentStrategyUpdateCELErrorReplacement(t *testing.T
celErrorNoteID, updatedNoteID)
}
+func TestGitlabMRSkippedStatusReported(t *testing.T) {
+ topts := &tgitlab.TestOpts{
+ NoMRCreation: true,
+ SkipEventsCheck: true,
+ Settings: &v1alpha1.Settings{
+ StatusChecks: &v1alpha1.StatusChecks{
+ Enabled: true,
+ Mode: v1alpha1.StatusCheckModePerPipelineRun,
+ },
+ },
+ }
+ ctx, cleanup := tgitlab.TestMR(t, topts)
+ defer cleanup()
+
+ entries, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-matching.yaml": "testdata/pipelinerun.yaml"},
+ topts.TargetNS, topts.DefaultBranch, triggertype.PullRequest.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+
+ // this is not going to match as it's targeting main branch on push event while we're gonna raise a pull request
+ skipEntry, err := payload.GetEntries(
+ map[string]string{".tekton/pipelinerun-skipped.yaml": "testdata/pipelinerun.yaml"},
+ topts.TargetNS, topts.DefaultBranch, triggertype.Push.String(), map[string]string{},
+ )
+ assert.NilError(t, err)
+ entries[".tekton/pipelinerun-skipped.yaml"] = skipEntry[".tekton/pipelinerun-skipped.yaml"]
+
+ scmOpts := &scm.Opts{
+ GitURL: topts.GitCloneURL,
+ Log: topts.ParamsRun.Clients.Log,
+ WebURL: topts.GitHTMLURL,
+ TargetRefName: topts.TargetRefName,
+ BaseRefName: topts.DefaultBranch,
+ }
+ topts.SHA = scm.PushFilesToRefGit(t, scmOpts, entries)
+
+ mrTitle := "TestMergeRequest - " + topts.TargetRefName
+ mrID, err := tgitlab.CreateMR(topts.GLProvider.Client(), topts.ProjectID, topts.TargetRefName, topts.DefaultBranch, mrTitle)
+ assert.NilError(t, err)
+ topts.MRNumber = mrID
+ topts.ParamsRun.Clients.Log.Infof("MergeRequest %s/-/merge_requests/%d has been created", topts.GitHTMLURL, mrID)
+
+ sopt := twait.SuccessOpt{
+ TargetNS: topts.TargetNS,
+ OnEvent: "Merge Request",
+ NumberofPRMatch: 1,
+ MinNumberStatus: 1,
+ }
+ twait.Succeeded(ctx, t, topts.ParamsRun, topts.Opts, sopt)
+
+ mr, _, err := topts.GLProvider.Client().MergeRequests.GetMergeRequest(topts.ProjectID, int64(topts.MRNumber), nil)
+ assert.NilError(t, err)
+
+ commitStatuses, _, err := topts.GLProvider.Client().Commits.GetCommitStatuses(topts.ProjectID, mr.SHA, &clientGitlab.GetCommitStatusesOptions{})
+ assert.NilError(t, err)
+
+ foundStatus := false
+ for _, cs := range commitStatuses {
+ if cs.Status == "skipped" && strings.Contains(cs.Name, "pipelinerun-skipped") {
+ foundStatus = true
+ break
+ }
+ }
+ assert.Equal(t, foundStatus, true, "should have found the skipped status for the non-matching pipeline run")
+}
+
// Local Variables:
// compile-command: "go test -tags=e2e -v -run ^TestGitlabMergeRequest$"
// End:
diff --git a/test/pkg/bitbucketcloud/crd.go b/test/pkg/bitbucketcloud/crd.go
index 6297a7cc50..d7e79cf8c2 100644
--- a/test/pkg/bitbucketcloud/crd.go
+++ b/test/pkg/bitbucketcloud/crd.go
@@ -35,7 +35,8 @@ func CreateCRD(ctx context.Context, t *testing.T, bprovider bitbucketcloud.Provi
Name: targetNS,
},
Spec: v1alpha1.RepositorySpec{
- URL: links.HTML.HRef,
+ URL: links.HTML.HRef,
+ Settings: opts.Settings,
},
}
err = pacrepo.CreateNS(ctx, targetNS, run)
diff --git a/test/pkg/bitbucketdatacenter/crd.go b/test/pkg/bitbucketdatacenter/crd.go
index bade6a062f..46a2ae2365 100644
--- a/test/pkg/bitbucketdatacenter/crd.go
+++ b/test/pkg/bitbucketdatacenter/crd.go
@@ -8,6 +8,7 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
+ "github.com/openshift-pipelines/pipelines-as-code/test/pkg/options"
pacrepo "github.com/openshift-pipelines/pipelines-as-code/test/pkg/repository"
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/secret"
@@ -16,7 +17,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
-func CreateCRD(ctx context.Context, t *testing.T, client *scm.Client, run *params.Run, orgAndRepo, targetNS string) *scm.Repository {
+func CreateCRD(ctx context.Context, t *testing.T, client *scm.Client, run *params.Run, opts options.E2E, orgAndRepo, targetNS string) *scm.Repository {
repo, resp, err := client.Repositories.Find(ctx, orgAndRepo)
assert.NilError(t, err, "error getting repository: http status code: %d: %v", resp.Status, err)
@@ -26,7 +27,8 @@ func CreateCRD(ctx context.Context, t *testing.T, client *scm.Client, run *param
Name: targetNS,
},
Spec: v1alpha1.RepositorySpec{
- URL: url,
+ URL: url,
+ Settings: opts.Settings,
},
}