Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Before getting started with Pipelines-as-Code, ensure you have:
- **Multi-provider support**: Works with GitHub (via GitHub App & Webhook), GitLab, Gitea, Bitbucket Data Center & Cloud via webhooks.
- **Annotation-driven workflows**: Target specific events, branches, or CEL expressions and gate untrusted PRs with `/ok-to-test` and `OWNERS`; see [Running the PipelineRun](https://pipelinesascode.com/docs/guide/running/).
- **ChatOps style control**: `/test`, `/retest`, `/cancel`, and branch or tag selectors let you rerun or stop PipelineRuns from PR comments or commit messages; see [GitOps Commands](https://pipelinesascode.com/docs/guide/gitops_commands/).
- **Skip CI support**: Use `[skip ci]`, `[ci skip]`, `[skip tkn]`, or `[tkn skip]` in commit messages to skip automatic PipelineRun execution for documentation updates or minor changes; GitOps commands can still override and trigger runs manually; see [Skip CI Commands](https://pipelinesascode.com/docs/guide/gitops_commands/#skip-ci-commands).
- **Feedback**: GitHub Checks capture per-task timing, log snippets, and optional error annotations while redacting secrets; see [PipelineRun status](https://pipelinesascode.com/docs/guide/statuses/).
- **Inline resolution**: The resolver bundles `.tekton/` resources, inlines remote tasks from Artifact Hub or Tekton Hub, and validates YAML before cluster submission; see [Resolver](https://pipelinesascode.com/docs/guide/resolver/).
- **CLI**: `tkn pac` bootstraps installs, manages Repository CRDs, inspects logs, and resolves runs locally; see the [CLI guide](https://pipelinesascode.com/docs/guide/cli/).
Expand Down
91 changes: 91 additions & 0 deletions docs/content/docs/guide/matchingevents.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,94 @@ main and the branch called `release,nightly` you can do this:
```yaml
pipelinesascode.tekton.dev/on-target-branch: [main, release,nightly]
```

## Skip CI Commands

Pipelines-as-Code supports skip commands in commit messages that allow you to skip
PipelineRun execution for specific commits. This is useful when making documentation
changes, minor fixes, or work-in-progress commits where running the full CI pipeline
is unnecessary.

### Supported Skip Commands

You can include any of the following commands anywhere in your commit message to skip
PipelineRun execution:

* `[skip ci]` - Skip continuous integration
* `[ci skip]` - Alternative format for skipping CI
* `[skip tkn]` - Skip Tekton PipelineRuns
* `[tkn skip]` - Alternative format for skipping Tekton

**Note:** Skip commands are **case-sensitive** and must be in lowercase with brackets.

### Example Usage

```text
docs: update README with installation instructions [skip ci]
```

or

```text
WIP: refactor authentication module
This is still in progress and not ready for testing yet.
[ci skip]
```

### How Skip Commands Work

When a commit message contains a skip command:

1. **Pull Requests**: No PipelineRuns will be created when the PR is opened or updated and HEAD commit contains skip command.
2. **Push Events**: No PipelineRuns will be created when pushing to a branch with that commit message

### GitOps Commands Override Skip CI

**Important:** Skip CI commands can be overridden by using GitOps commands. Even if
a commit contains a skip command like `[skip ci]`, you can still manually trigger
PipelineRuns using:

* `/test` - Trigger all matching PipelineRuns
* `/test <pipelinerun-name>` - Trigger a specific PipelineRun
* `/retest` - Retrigger failed PipelineRuns
* `/retest <pipelinerun-name>` - Retrigger a specific PipelineRun
* `/ok-to-test` - Allow running CI for external contributors
* `/custom-comment` - Trigger PipelineRun having on-comment annotation

This allows you to skip automatic CI execution while still maintaining the ability
to manually trigger builds when needed.

### Example: Skipping CI Then Manually Triggering

```bash
# Initial commit with skip command
git commit -m "docs: update contributing guide [skip ci]"
git push origin my-feature-branch
# No PipelineRuns are created automatically
# Later, you can manually trigger CI by commenting on the PR:
# /test
# This will create PipelineRuns despite the [skip ci] command
```

### Examples of When to Use Skip Commands

Skip commands are useful for:

* Documentation-only changes
* README updates
* Comment or formatting changes
* Work-in-progress commits
* Minor typo fixes
* Configuration file updates that don't affect code

### Examples of When NOT to Use Skip Commands

Avoid using skip commands for:

* Code changes that affect functionality
* Changes to CI/CD pipeline definitions
* Dependency updates
* Any changes that should be tested before merging
72 changes: 72 additions & 0 deletions pkg/adapter/sinker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package adapter
import (
"bytes"
"context"
"fmt"
"net/http"

"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction"
"github.com/openshift-pipelines/pipelines-as-code/pkg/matcher"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
"github.com/openshift-pipelines/pipelines-as-code/pkg/pipelineascode"
Expand Down Expand Up @@ -69,8 +71,78 @@ func (s *sinker) processEvent(ctx context.Context, request *http.Request) error
if err := s.processEventPayload(ctx, request); err != nil {
return err
}

// For ALL events: Setup authenticated client early (including token scoping)
// This centralizes client setup and token scoping in one place for all event types
repo, err := s.findMatchingRepository(ctx)
if err != nil {
// Continue with normal flow - repository matching will be handled in matchRepoPR
s.logger.Debugf("Could not find matching repository for early client setup: %v", err)
} else {
// We found the repository, now setup client with token scoping
// If setup fails here, it's a configuration error and we should fail fast
if err := s.setupClient(ctx, repo); err != nil {
return fmt.Errorf("client setup failed: %w", err)
}
s.logger.Debugf("Client setup completed early in sinker for event type: %s", s.event.EventType)
}

// For PUSH events: commit message is already in event.SHATitle from the webhook payload
// We can check immediately without any API calls or repository lookups
if s.event.EventType == "push" && provider.SkipCI(s.event.SHATitle) {
s.logger.Infof("CI skipped for push event: commit %s contains skip command in message", s.event.SHA)
return nil
}

// For PULL REQUEST events: commit message needs to be fetched via API
// Get commit info for skip-CI detection (only if we successfully set up client above)
if s.event.EventType == "pull_request" && repo != nil {
// Get commit info (including commit message) via API
if err := s.vcx.GetCommitInfo(ctx, s.event); err != nil {
return fmt.Errorf("could not get commit info: %w", err)
}
// Check for skip-ci commands in pull request events
if s.event.HasSkipCommand {
s.logger.Infof("CI skipped for pull request event: commit %s contains skip command in message", s.event.SHA)
return nil
}
}
Comment on lines +74 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I've said before, you can move this to ParsePayload, as name suggests ParsePayload func is to verify the payload

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this logic was common for all providers, I kept it here to avoid duplication.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, but there is one more call to GetCommitInfo then that's not needed to avoid more API calls

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of calls which are duplicated in this code at the moment. I have highlighted some of them in my comments, looking for inputs for how useful (or expensive) they can be.

As for having GetCommitInfo only once, I would personally prefer having it here (moving it outside the pull_request conditional of course) rather than duplicating this logic for each provider. (And removing from match.go if we want to avoid multiple API calls.)

Open to hearing contrary perspective to this.

Copy link
Contributor

@zakisk zakisk Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And removing from match.go if we want to avoid multiple API calls.

yeah, removing would work. and also call get_commit endpoint only if one of the field SHA, SHATitle, and ShaURL is empty so that we can avoid API call for GitLab etc.

if event.SHA == "" || event.SHATitle == "" || event.SHAURL == ""

}

p := pipelineascode.NewPacs(s.event, s.vcx, s.run, s.pacInfo, s.kint, s.logger, s.globalRepo)
return p.Run(ctx)
}

// findMatchingRepository finds the Repository CR that matches the event.
// This is a lightweight lookup to get credentials for early skip-ci checks.
// Uses the canonical matcher implementation to avoid code duplication.
func (s *sinker) findMatchingRepository(ctx context.Context) (*v1alpha1.Repository, error) {
// Use canonical matcher to find repository (empty string searches all namespaces)
repo, err := matcher.MatchEventURLRepo(ctx, s.run, s.event, "")
if err != nil {
return nil, fmt.Errorf("failed to match repository: %w", err)
}
if repo == nil {
return nil, fmt.Errorf("no repository found matching URL: %s", s.event.URL)
}

return repo, nil
}

// setupClient sets up the authenticated client with token scoping for ALL event types.
// This is the primary location where client setup and GitHub App token scoping happens.
// Centralizing this here ensures consistent behavior across all events and enables early
// optimizations like skip-CI detection before expensive processing.
func (s *sinker) setupClient(ctx context.Context, repo *v1alpha1.Repository) error {
return pipelineascode.SetupAuthenticatedClient(
ctx,
s.vcx,
s.kint,
s.run,
s.event,
repo,
s.globalRepo,
s.pacInfo,
s.logger,
)
}
Loading