Skip to content

Commit 84ea887

Browse files
committed
feat: add skip CI command support
Implement support for skip CI commands in commit messages to allow users to skip PipelineRun execution. Supports [skip ci], [ci skip], [skip tkn], and [tkn skip] commands. When a skip command is detected in the commit message, PipelineRun execution is skipped. However, GitOps commands (/test, /retest, etc.) will still trigger PipelineRuns regardless of the skip command, allowing users to manually trigger CI when needed. Jira: https://issues.redhat.com/browse/SRVKP-8933 Signed-off-by: Akshay Pant <akshay.akshaypant@gmail.com>
1 parent f3db57f commit 84ea887

File tree

15 files changed

+585
-7
lines changed

15 files changed

+585
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Before getting started with Pipelines-as-Code, ensure you have:
6565
- **Multi-provider support**: Works with GitHub (via GitHub App & Webhook), GitLab, Gitea, Bitbucket Data Center & Cloud via webhooks.
6666
- **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/).
6767
- **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/).
68+
- **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).
6869
- **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/).
6970
- **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/).
7071
- **CLI**: `tkn pac` bootstraps installs, manages Repository CRDs, inspects logs, and resolves runs locally; see the [CLI guide](https://pipelinesascode.com/docs/guide/cli/).

docs/content/docs/guide/gitops_commands.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,89 @@ Pipelines-as-Code supports the concept of `GitOps commands`, which allow users t
99

1010
The advantage of using a `GitOps command` is that it provides a journal of all the executions of your pipeline directly on your Pull Request, near your code.
1111

12+
## Skip CI Commands
13+
14+
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.
15+
16+
### Supported Skip Commands
17+
18+
You can include any of the following commands anywhere in your commit message to skip PipelineRun execution:
19+
20+
- `[skip ci]` - Skip continuous integration
21+
- `[ci skip]` - Alternative format for skipping CI
22+
- `[skip tkn]` - Skip Tekton PipelineRuns
23+
- `[tkn skip]` - Alternative format for skipping Tekton
24+
25+
**Note:** Skip commands are **case-sensitive** and must be in lowercase with brackets.
26+
27+
### Example Usage
28+
29+
```text
30+
docs: update README with installation instructions [skip ci]
31+
```
32+
33+
or
34+
35+
```text
36+
WIP: refactor authentication module
37+
38+
This is still in progress and not ready for testing yet.
39+
40+
[ci skip]
41+
```
42+
43+
### How Skip Commands Work
44+
45+
When a commit message contains a skip command:
46+
47+
1. **Pull Requests**: No PipelineRuns will be created when the PR is opened or updated with that commit
48+
2. **Push Events**: No PipelineRuns will be created when pushing to a branch with that commit message
49+
50+
### GitOps Commands Override Skip CI
51+
52+
**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:
53+
54+
- `/test` - Trigger all matching PipelineRuns
55+
- `/test <pipelinerun-name>` - Trigger a specific PipelineRun
56+
- `/retest` - Retrigger failed PipelineRuns
57+
- `/retest <pipelinerun-name>` - Retrigger a specific PipelineRun
58+
- `/ok-to-test` - Allow running CI for external contributors
59+
60+
This allows you to skip automatic CI execution while still maintaining the ability to manually trigger builds when needed.
61+
62+
### Example: Skipping CI Then Manually Triggering
63+
64+
```bash
65+
# Initial commit with skip command
66+
git commit -m "docs: update contributing guide [skip ci]"
67+
git push origin my-feature-branch
68+
# No PipelineRuns are created automatically
69+
70+
# Later, you can manually trigger CI by commenting on the PR:
71+
# /test
72+
# This will create PipelineRuns despite the [skip ci] command
73+
```
74+
75+
### When to Use Skip Commands
76+
77+
Skip commands are useful for:
78+
79+
- Documentation-only changes
80+
- README updates
81+
- Comment or formatting changes
82+
- Work-in-progress commits
83+
- Minor typo fixes
84+
- Configuration file updates that don't affect code
85+
86+
### When NOT to Use Skip Commands
87+
88+
Avoid using skip commands for:
89+
90+
- Code changes that affect functionality
91+
- Changes to CI/CD pipeline definitions
92+
- Dependency updates
93+
- Any changes that should be tested before merging
94+
1295
## GitOps Commands on Pull Requests
1396

1497
For example, when you are on a Pull Request, you may want to restart failed PipelineRuns. To do so, you can add a comment on your Pull Request starting with `/retest`, and all **failed** PipelineRuns attached to that Pull Request will be restarted. If all previous PipelineRuns for the same commit were successful, no new PipelineRuns will be created to avoid unnecessary duplication.

docs/content/docs/guide/incoming_webhook.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ spec:
115115
params:
116116
- prod_env
117117
type: webhook-url
118-
118+
119119
# Feature branches - checked second
120120
- targets:
121121
- "feature/*"
@@ -125,7 +125,7 @@ spec:
125125
params:
126126
- dev_env
127127
type: webhook-url
128-
128+
129129
# Catch-all - checked last
130130
- targets:
131131
- "*" # Matches any branch not caught above

pkg/params/info/events.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ type Event struct {
4444
PullRequestLabel []string // Labels of the pull Request
4545
TriggerComment string // The comment triggering the pipelinerun when using on-comment annotation
4646

47+
// HasSkipCommand indicates whether the commit message contains a skip CI command
48+
// (e.g., [skip ci], [ci skip], [skip tkn], [tkn skip]). When true, PipelineRun
49+
// execution will be skipped unless overridden by a GitOps command (e.g., /test, /retest).
50+
// This allows users to bypass CI for documentation changes or minor fixes while still
51+
// maintaining the ability to manually trigger builds when needed.
52+
HasSkipCommand bool
53+
4754
// TODO: move forge specifics to each driver
4855
// Github
4956
Organization string

pkg/pipelineascode/pipelineascode.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
1414
"github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction"
1515
"github.com/openshift-pipelines/pipelines-as-code/pkg/matcher"
16+
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
1617
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1718
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
1819
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/settings"
@@ -89,7 +90,14 @@ func (p *PacRun) Run(ctx context.Context) error {
8990
if repo.Spec.ConcurrencyLimit != nil && *repo.Spec.ConcurrencyLimit != 0 {
9091
p.manager.Enable()
9192
}
92-
93+
// Check skip logic after annotation matching, so that the EventType has been properly updated.
94+
// In order for the PR to be skipped, the commit message should include the skip command.
95+
// We don't want to skip the PR when the event is a GitOps command. For example, if the
96+
// user has added `[skip ci]` to the commit, they don't need to push again to trigger the
97+
// PipelineRun, they can simply add a comment to trigger the CI via `/test pr-name` or on-comment.
98+
if p.event.HasSkipCommand && !opscomments.IsAnyOpsEventType(p.event.EventType) {
99+
return nil
100+
}
93101
// set params for the console driver, only used for the custom console ones
94102
cp := customparams.NewCustomParams(p.event, repo, p.run, p.k8int, p.eventEmitter, p.vcx)
95103
maptemplate, _, err := cp.GetParams(ctx)

pkg/pipelineascode/pipelineascode_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,171 @@ func TestRun(t *testing.T) {
581581
finalStatusText: "creating basic auth secret",
582582
secretCreationError: fmt.Errorf("connection timeout"),
583583
},
584+
{
585+
name: "skip ci/skip ci command in commit message for pull_request",
586+
runevent: info.Event{
587+
Event: &github.PullRequestEvent{
588+
PullRequest: &github.PullRequest{
589+
Number: github.Ptr(666),
590+
},
591+
},
592+
SHA: "fromwebhook",
593+
Organization: "owner",
594+
Sender: "owner",
595+
Repository: "repo",
596+
URL: "https://service/documentation",
597+
HeadBranch: "press",
598+
BaseBranch: "main",
599+
EventType: "pull_request",
600+
TriggerTarget: "pull_request",
601+
PullRequestNumber: 666,
602+
InstallationID: 1234,
603+
HasSkipCommand: true,
604+
},
605+
tektondir: "testdata/pull_request",
606+
finalStatus: "skipped",
607+
},
608+
{
609+
name: "skip ci/ci skip command in commit message for push",
610+
runevent: info.Event{
611+
SHA: "principale",
612+
Organization: "organizationes",
613+
Repository: "lagaffe",
614+
URL: "https://service/documentation",
615+
HeadBranch: "refs/heads/main",
616+
BaseBranch: "refs/heads/main",
617+
Sender: "fantasio",
618+
EventType: "push",
619+
TriggerTarget: "push",
620+
HasSkipCommand: true,
621+
},
622+
tektondir: "testdata/push_branch",
623+
finalStatus: "skipped",
624+
},
625+
{
626+
name: "skip ci/skip tkn command in commit message",
627+
runevent: info.Event{
628+
Event: &github.PullRequestEvent{
629+
PullRequest: &github.PullRequest{
630+
Number: github.Ptr(666),
631+
},
632+
},
633+
SHA: "fromwebhook",
634+
Organization: "owner",
635+
Sender: "owner",
636+
Repository: "repo",
637+
URL: "https://service/documentation",
638+
HeadBranch: "press",
639+
BaseBranch: "main",
640+
EventType: "pull_request",
641+
TriggerTarget: "pull_request",
642+
PullRequestNumber: 666,
643+
InstallationID: 1234,
644+
HasSkipCommand: true,
645+
},
646+
tektondir: "testdata/pull_request",
647+
finalStatus: "skipped",
648+
},
649+
{
650+
name: "skip ci/gitops command overrides skip",
651+
runevent: info.Event{
652+
Event: &github.PullRequestEvent{
653+
PullRequest: &github.PullRequest{
654+
Number: github.Ptr(666),
655+
},
656+
},
657+
SHA: "fromwebhook",
658+
Organization: "owner",
659+
Sender: "owner",
660+
Repository: "repo",
661+
URL: "https://service/documentation",
662+
HeadBranch: "press",
663+
BaseBranch: "main",
664+
EventType: "test-comment",
665+
TriggerTarget: "pull_request",
666+
PullRequestNumber: 666,
667+
InstallationID: 1234,
668+
HasSkipCommand: true,
669+
},
670+
tektondir: "testdata/pull_request",
671+
finalStatus: "neutral",
672+
finalStatusText: "<th>Status</th><th>Duration</th><th>Name</th>",
673+
},
674+
{
675+
name: "skip ci/retest command overrides skip",
676+
runevent: info.Event{
677+
Event: &github.PullRequestEvent{
678+
PullRequest: &github.PullRequest{
679+
Number: github.Ptr(666),
680+
},
681+
},
682+
SHA: "fromwebhook",
683+
Organization: "owner",
684+
Sender: "owner",
685+
Repository: "repo",
686+
URL: "https://service/documentation",
687+
HeadBranch: "press",
688+
BaseBranch: "main",
689+
EventType: "retest-comment",
690+
TriggerTarget: "pull_request",
691+
PullRequestNumber: 666,
692+
InstallationID: 1234,
693+
HasSkipCommand: true,
694+
},
695+
tektondir: "testdata/pull_request",
696+
finalStatus: "neutral",
697+
finalStatusText: "<th>Status</th><th>Duration</th><th>Name</th>",
698+
},
699+
{
700+
name: "skip ci/ok-to-test command overrides skip",
701+
runevent: info.Event{
702+
Event: &github.PullRequestEvent{
703+
PullRequest: &github.PullRequest{
704+
Number: github.Ptr(666),
705+
},
706+
},
707+
SHA: "fromwebhook",
708+
Organization: "owner",
709+
Sender: "owner",
710+
Repository: "repo",
711+
URL: "https://service/documentation",
712+
HeadBranch: "press",
713+
BaseBranch: "main",
714+
EventType: "ok-to-test-comment",
715+
TriggerTarget: "pull_request",
716+
PullRequestNumber: 666,
717+
InstallationID: 1234,
718+
HasSkipCommand: true,
719+
},
720+
tektondir: "testdata/pull_request",
721+
finalStatus: "neutral",
722+
finalStatusText: "<th>Status</th><th>Duration</th><th>Name</th>",
723+
},
724+
{
725+
name: "skip ci/no skip command normal execution",
726+
runevent: info.Event{
727+
Event: &github.PullRequestEvent{
728+
PullRequest: &github.PullRequest{
729+
Number: github.Ptr(666),
730+
},
731+
},
732+
SHA: "fromwebhook",
733+
Organization: "owner",
734+
Sender: "owner",
735+
Repository: "repo",
736+
URL: "https://service/documentation",
737+
HeadBranch: "press",
738+
BaseBranch: "main",
739+
EventType: "pull_request",
740+
TriggerTarget: "pull_request",
741+
PullRequestNumber: 666,
742+
InstallationID: 1234,
743+
HasSkipCommand: false,
744+
},
745+
tektondir: "testdata/pull_request",
746+
finalStatus: "neutral",
747+
finalStatusText: "<th>Status</th><th>Duration</th><th>Name</th>",
748+
},
584749
}
585750
for _, tt := range tests {
586751
t.Run(tt.name, func(t *testing.T) {

pkg/provider/bitbucketcloud/bitbucket.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ func (v *Provider) GetCommitInfo(_ context.Context, event *info.Event) error {
250250
event.SHATitle = commitinfo.Message
251251
event.SHAURL = commitinfo.Links.HTML.HRef
252252
event.SHA = commitinfo.Hash
253-
253+
event.HasSkipCommand = provider.SkipCI(commitinfo.Message)
254254
// now to get the default branch from repository.Get
255255
repo, err := v.Client().Repositories.Repository.Get(&bitbucket.RepositoryOptions{
256256
Owner: event.Organization,

pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ func (v *Provider) GetCommitInfo(_ context.Context, event *info.Event) error {
339339
}
340340
event.SHATitle = sanitizeTitle(commit.Message)
341341
event.SHAURL = fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", v.baseURL, v.projectKey, event.Repository, event.SHA)
342+
event.HasSkipCommand = provider.SkipCI(commit.Message)
342343

343344
ref, _, err := v.Client().Git.GetDefaultBranch(context.Background(), OrgAndRepo)
344345
if err != nil {

pkg/provider/gitea/gitea.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,8 @@ func (v *Provider) GetCommitInfo(_ context.Context, runevent *info.Event) error
377377
runevent.SHAURL = commit.HTMLURL
378378
runevent.SHATitle = strings.Split(commit.RepoCommit.Message, "\n\n")[0]
379379
runevent.SHA = commit.SHA
380+
runevent.HasSkipCommand = provider.SkipCI(commit.RepoCommit.Message)
381+
380382
return nil
381383
}
382384

pkg/provider/github/github.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ func (v *Provider) GetCommitInfo(ctx context.Context, runevent *info.Event) erro
406406
runevent.SHAURL = commit.GetHTMLURL()
407407
runevent.SHATitle = strings.Split(commit.GetMessage(), "\n\n")[0]
408408
runevent.SHA = commit.GetSHA()
409+
runevent.HasSkipCommand = provider.SkipCI(commit.GetMessage())
409410

410411
return nil
411412
}

0 commit comments

Comments
 (0)