Skip to content

Commit fbb4d9e

Browse files
zakiskchmouel
authored andcommitted
feat: Support PipelineRun re-triggering on git tag
- Add tag-aware GitOps parsing to handle `/test ... tag:<TAG>` - Support commit comments on tag commits in provider flow - Retrigger matching PipelineRun using the tag's commit SHA - Add/extend unit tests for tag parsing and provider behavior - Add docs about the feature - E2E: add TestGithubGitOpsCommentOnTag and tag helper - Add test data: test/testdata/pipelinerun-on-tag.yaml - Minor cleanups in test helpers and github_push_test.go - No API changes; default behavior unchanged Signed-off-by: Zaki Shaikh <zashaikh@redhat.com>
1 parent 018026a commit fbb4d9e

File tree

9 files changed

+430
-41
lines changed

9 files changed

+430
-41
lines changed

docs/content/docs/guide/gitops_commands.md

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: GitOps Commands
33
weight: 5.1
44
---
5+
56
# GitOps Commands
67

78
Pipelines-as-Code supports the concept of `GitOps commands`, which allow users to issue special commands in a comment on a Pull Request or a pushed commit to control `Pipelines-as-Code`.
@@ -59,6 +60,7 @@ This means:
5960
1. When a user comments with commands like `/retest` or `/test` on a branch without specifying a branch name, the test will automatically run on the **default branch** (e.g. main, master) of the repository.
6061

6162
Examples:
63+
6264
1. `/retest`
6365
2. `/test`
6466
3. `/retest <pipelinerun-name>`
@@ -67,6 +69,7 @@ This means:
6769
2. If the user includes a branch specification such as `/retest branch:test` or `/test branch:test`, the test will be executed on the commit where the comment is located, with the context of the **test** branch.
6870

6971
Examples:
72+
7073
1. `/retest branch:test`
7174
2. `/test branch:test`
7275
3. `/retest <pipelinerun-name> branch:test`
@@ -102,6 +105,81 @@ Please note that this feature is supported for the GitHub provider only.
102105

103106
The PipelineRun will be restarted regardless of the annotations if the comment `/test <pipelinerun-name>` or `/retest <pipelinerun-name>` is used. This allows you to have control of PipelineRuns that get only triggered by a comment on the Pull Request.
104107

108+
### Triggering PipelineRun on Git tags
109+
110+
{{< support_matrix github_app="true" github_webhook="true" gitea="false" gitlab="false" bitbucket_cloud="false" bitbucket_server="false" >}}
111+
112+
You can retrigger a PipelineRun against a specific Git tag by commenting on
113+
the tagged commit using a GitOps command. Pipelines-as-Code will resolve the
114+
tag to its commit SHA and run the matching PipelineRun against that commit.
115+
116+
Supported commands:
117+
118+
- `/test tag:<tag>`: retrigger all matching PipelineRuns for the tag commit
119+
- `/test <pipelinerun-name> tag:<tag>`: retrigger only the named PipelineRun
120+
- `/retest tag:<tag>`: retrigger all matching PipelineRuns for the tag commit
121+
- `/retest <pipelinerun-name> tag:<tag>`: retrigger only the named PipelineRun
122+
- `/cancel tag:<tag>`: cancel all running PipelineRuns for the tag commit
123+
- `/cancel <pipelinerun-name> tag:<tag>`: cancel only the named PipelineRun
124+
125+
Examples:
126+
127+
```text
128+
/test tag:v1.0.0
129+
```
130+
131+
or
132+
133+
```text
134+
/retest tag:v1.0.0
135+
```
136+
137+
```text
138+
/cancel tag:v1.0.0
139+
```
140+
141+
```text
142+
/cancel pipelinerun-on-tag tag:v1.0.0
143+
```
144+
145+
```text
146+
/test pipelinerun-on-tag tag:v1.0.0
147+
```
148+
149+
Notes:
150+
151+
- The event type is treated as `push`; configure your PipelineRun with
152+
`pipelinesascode.tekton.dev/on-event: "[push]"`.
153+
- This is currently supported on GitHub (App and Webhook) only.
154+
155+
Minimal PipelineRun example:
156+
157+
```yaml
158+
apiVersion: tekton.dev/v1beta1
159+
kind: PipelineRun
160+
metadata:
161+
name: pipelinerun-on-tag
162+
annotations:
163+
pipelinesascode.tekton.dev/on-target-branch: "[refs/tags/*]"
164+
pipelinesascode.tekton.dev/on-event: "[push]"
165+
spec:
166+
pipelineSpec:
167+
tasks:
168+
- name: tag-task
169+
taskSpec:
170+
steps:
171+
- name: echo
172+
image: registry.access.redhat.com/ubi9/ubi-micro
173+
script: |
174+
echo "tag: {{ git_tag }}"
175+
```
176+
177+
How to comment on a tag commit (GitHub):
178+
179+
1. Go to your repository and open the Tags view (or Releases).
180+
2. Click the tag (for example, `v1.0.0`) to navigate to its commit.
181+
3. Add a comment on the commit with one of the commands above.
182+
105183
## Accessing the Comment Triggering the PipelineRun
106184

107185
When you trigger a PipelineRun via a GitOps Command, the template variable `{{
@@ -204,12 +282,14 @@ This means:
204282
1. If a user specifies commands like `/cancel` without any argument in a comment on a branch, it will automatically target the **main** branch.
205283

206284
Examples:
285+
207286
1. `/cancel`
208287
2. `/cancel <pipelinerun-name>`
209288

210289
2. If the user issues a command like `/cancel branch:test`, it will target the commit where the comment was made but use the **test** branch.
211290

212291
Examples:
292+
213293
1. `/cancel branch:test`
214294
2. `/cancel <pipelinerun-name> branch:test`
215295

@@ -241,10 +321,10 @@ You can pass those `key=value` pairs anywhere in your comment, and they will be
241321

242322
There are different formats that can be accepted, allowing you to pass values with spaces or newlines:
243323

244-
* key=value
245-
* key="a value"
246-
* key="another \"value\" defined"
247-
* key="another
324+
- key=value
325+
- key="a value"
326+
- key="another \"value\" defined"
327+
- key="another
248328
value with newline"
249329

250330
## Event Type Annotation and Dynamic Variables
@@ -253,14 +333,14 @@ The `pipeline.tekton.dev/event-type` annotation indicates the type of GitOps com
253333

254334
Here are the possible event types:
255335

256-
* `test-all-comment`: The event is a single `/test` that would test every matched PipelineRun.
257-
* `test-comment`: The event is a `/test <PipelineRun>` comment that would test a specific PipelineRun.
258-
* `retest-all-comment`: The event is a single `/retest` that would retest every matched PipelineRun.
259-
* `retest-comment`: The event is a `/retest <PipelineRun>` that would retest a specific PipelineRun.
260-
* `on-comment`: The event is coming from a custom comment that would trigger a PipelineRun.
261-
* `cancel-all-comment`: The event is a single `/cancel` that would cancel every matched PipelineRun.
262-
* `cancel-comment`: The event is a `/cancel <PipelineRun>` that would cancel a specific PipelineRun.
263-
* `ok-to-test-comment`: The event is a `/ok-to-test` that would allow running the CI for an unauthorized user.
336+
- `test-all-comment`: The event is a single `/test` that would test every matched PipelineRun.
337+
- `test-comment`: The event is a `/test <PipelineRun>` comment that would test a specific PipelineRun.
338+
- `retest-all-comment`: The event is a single `/retest` that would retest every matched PipelineRun.
339+
- `retest-comment`: The event is a `/retest <PipelineRun>` that would retest a specific PipelineRun.
340+
- `on-comment`: The event is coming from a custom comment that would trigger a PipelineRun.
341+
- `cancel-all-comment`: The event is a single `/cancel` that would cancel every matched PipelineRun.
342+
- `cancel-comment`: The event is a `/cancel <PipelineRun>` that would cancel a specific PipelineRun.
343+
- `ok-to-test-comment`: The event is a `/ok-to-test` that would allow running the CI for an unauthorized user.
264344

265345
If a repository owner comments `/ok-to-test` on a pull request from an external contributor but no PipelineRun **matches** the `pull_request` event (or the repository has no `.tekton` directory), Pipelines-as-Code sets a **neutral** commit status. This indicates that no PipelineRun was matched, allowing other workflows—such as auto-merge—to proceed without being blocked.
266346

@@ -272,12 +352,12 @@ Note: This neutral check-run status functionality is only supported on GitHub.
272352

273353
When using the `{{ event_type }}` [dynamic variable]({{< relref "/docs/guide/authoringprs.md#dynamic-variables" >}}) for the following event types:
274354

275-
* `test-all-comment`
276-
* `test-comment`
277-
* `retest-all-comment`
278-
* `retest-comment`
279-
* `cancel-all-comment`
280-
* `ok-to-test-comment`
355+
- `test-all-comment`
356+
- `test-comment`
357+
- `retest-all-comment`
358+
- `retest-comment`
359+
- `cancel-all-comment`
360+
- `ok-to-test-comment`
281361

282362
The dynamic variable will return `pull_request` as the event type instead of the specific categorized GitOps command type. This is to handle backward compatibility with previous releases for users relying on this dynamic variable.
283363

pkg/provider/github/parse_payload.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,13 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
543543
var (
544544
branchName string
545545
prName string
546+
tagName string
546547
err error
547548
)
548549

549550
// If it is a /test or /retest comment with pipelinerun name figure out the pipelinerun name
550551
if provider.IsTestRetestComment(event.GetComment().GetBody()) {
551-
prName, branchName, err = provider.GetPipelineRunAndBranchNameFromTestComment(event.GetComment().GetBody())
552+
prName, branchName, tagName, err = provider.GetPipelineRunAndBranchOrTagNameFromTestComment(event.GetComment().GetBody())
552553
if err != nil {
553554
return runevent, err
554555
}
@@ -557,14 +558,39 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
557558
// Check for /cancel comment
558559
if provider.IsCancelComment(event.GetComment().GetBody()) {
559560
action = "cancellation"
560-
prName, branchName, err = provider.GetPipelineRunAndBranchNameFromCancelComment(event.GetComment().GetBody())
561+
prName, branchName, tagName, err = provider.GetPipelineRunAndBranchOrTagNameFromCancelComment(event.GetComment().GetBody())
561562
if err != nil {
562563
return runevent, err
563564
}
564565
runevent.CancelPipelineRuns = true
565566
runevent.TargetCancelPipelineRun = prName
566567
}
567568

569+
if tagName != "" {
570+
tagPath := fmt.Sprintf("refs/tags/%s", tagName)
571+
// here in GitHub TAG_SHA and the commit which is tagged for a tag are different
572+
// so we need to get the ref for the tag and then get the tag object to get the tag SHA
573+
ref, _, err := wrapAPI(v, "get_ref", func() (*github.Reference, *github.Response, error) {
574+
return v.Client().Git.GetRef(ctx, runevent.Organization, runevent.Repository, tagPath)
575+
})
576+
if err != nil {
577+
return runevent, fmt.Errorf("error getting ref for tag %s: %w", tagName, err)
578+
}
579+
// get the tag object to get the SHA
580+
tag, _, err := wrapAPI(v, "get_tag", func() (*github.Tag, *github.Response, error) {
581+
return v.Client().Git.GetTag(ctx, runevent.Organization, runevent.Repository, ref.GetObject().GetSHA())
582+
})
583+
if err != nil {
584+
return runevent, fmt.Errorf("error getting tag %s: %w", tagName, err)
585+
}
586+
if tag.GetObject().GetSHA() != runevent.SHA {
587+
return runevent, fmt.Errorf("provided SHA %s is not the tagged commit for the tag %s", runevent.SHA, tagName)
588+
}
589+
runevent.HeadBranch = tagPath
590+
runevent.BaseBranch = tagPath
591+
return runevent, nil
592+
}
593+
568594
// If no branch is specified in GitOps comments, use runevent.HeadBranch
569595
if branchName == "" {
570596
branchName = runevent.HeadBranch

pkg/provider/github/parse_payload_test.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ func TestParsePayLoad(t *testing.T) {
369369
targetPipelinerun string
370370
targetCancelPipelinerun string
371371
wantedBranchName string
372+
wantedTagName string
372373
isCancelPipelineRunEnabled bool
373374
}{
374375
{
@@ -661,7 +662,7 @@ func TestParsePayLoad(t *testing.T) {
661662
},
662663
{
663664
name: "bad/commit comment for /test command does not contain branch keyword",
664-
wantErrString: "the GitOps comment dummy rbanch does not contain a branch word",
665+
wantErrString: "the GitOps comment `/test dummy rbanch:test` does not contain a branch or tag word",
665666
eventType: "commit_comment",
666667
triggerTarget: "push",
667668
githubClient: true,
@@ -713,6 +714,57 @@ func TestParsePayLoad(t *testing.T) {
713714
shaRet: "samplePRsha",
714715
wantedBranchName: "main",
715716
},
717+
{
718+
name: "good/commit comment for test with tag",
719+
eventType: "commit_comment",
720+
triggerTarget: "push",
721+
githubClient: true,
722+
payloadEventStruct: github.CommitCommentEvent{
723+
Repo: sampleRepo,
724+
Comment: &github.RepositoryComment{
725+
CommitID: github.Ptr("samplePRsha"),
726+
HTMLURL: github.Ptr("/777"),
727+
Body: github.Ptr("/test tag:v1.0.0"),
728+
},
729+
},
730+
shaRet: "samplePRsha",
731+
wantedBranchName: "refs/tags/v1.0.0",
732+
},
733+
{
734+
name: "good/commit comment for test with pipelinerun name and tag",
735+
eventType: "commit_comment",
736+
triggerTarget: "push",
737+
githubClient: true,
738+
payloadEventStruct: github.CommitCommentEvent{
739+
Repo: sampleRepo,
740+
Comment: &github.RepositoryComment{
741+
CommitID: github.Ptr("samplePRsha"),
742+
HTMLURL: github.Ptr("/777"),
743+
Body: github.Ptr("/test dummy tag:v1.0.0"),
744+
},
745+
},
746+
shaRet: "samplePRsha",
747+
targetPipelinerun: "dummy",
748+
wantedBranchName: "refs/tags/v1.0.0",
749+
},
750+
{
751+
name: "bad/commit comment for test with pipelinerun name and wrong tag keyword",
752+
eventType: "commit_comment",
753+
triggerTarget: "push",
754+
githubClient: true,
755+
payloadEventStruct: github.CommitCommentEvent{
756+
Repo: sampleRepo,
757+
Comment: &github.RepositoryComment{
758+
CommitID: github.Ptr("samplePRsha"),
759+
HTMLURL: github.Ptr("/777"),
760+
Body: github.Ptr("/test dummy taig:v1.0.0"),
761+
},
762+
},
763+
shaRet: "samplePRsha",
764+
targetPipelinerun: "dummy",
765+
wantedBranchName: "refs/tags/v1.0.0",
766+
wantErrString: "the GitOps comment `/test dummy taig:v1.0.0` does not contain a branch or tag word",
767+
},
716768
{
717769
name: "good/commit comment for cancel all",
718770
eventType: "commit_comment",
@@ -891,7 +943,27 @@ func TestParsePayLoad(t *testing.T) {
891943
}`)
892944
assert.NilError(t, err)
893945
})
946+
947+
mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/git/ref/tags/v1.0.0", "owner", "reponame"), func(rw http.ResponseWriter, _ *http.Request) {
948+
ref := &github.Reference{
949+
Object: &github.GitObject{
950+
SHA: github.Ptr("samplePRsha"),
951+
},
952+
}
953+
bjeez, _ := json.Marshal(ref)
954+
fmt.Fprint(rw, string(bjeez))
955+
})
956+
mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/git/tags/samplePRsha", "owner", "reponame"), func(rw http.ResponseWriter, _ *http.Request) {
957+
tag := &github.Tag{
958+
Object: &github.GitObject{
959+
SHA: github.Ptr("samplePRsha"),
960+
},
961+
}
962+
bjeez, _ := json.Marshal(tag)
963+
fmt.Fprint(rw, string(bjeez))
964+
})
894965
}
966+
895967
logger, _ := logger.GetLogger()
896968
gprovider := Provider{
897969
ghClient: ghClient,

0 commit comments

Comments
 (0)