Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support skipping stages by path pattern or commit message prefix #4922

Merged
merged 51 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
06c5f5d
rename func to distinguish the trigger of skip stage
t-kikuc Apr 22, 2024
022e940
Draft: impl check of skipping stage
t-kikuc Apr 22, 2024
59b14ab
Rename to 'prefixes'
t-kikuc Apr 29, 2024
691db8a
separate to sub funcs
t-kikuc Apr 29, 2024
7c37add
Impl Repo.GetCommitFromRev
t-kikuc Apr 29, 2024
ced6054
Impl skipByPathPattern()
t-kikuc Apr 29, 2024
3809d14
Add skipOn options to Wait Stage Options
t-kikuc Apr 29, 2024
7d7f9b7
Add check of skipping stage in Analysis,Wait,WaitApproval Stages
t-kikuc Apr 29, 2024
c4a529d
Fix comments
t-kikuc Apr 29, 2024
e925ef7
draft: add an empty test file
t-kikuc Apr 30, 2024
ee89a23
fix comments
t-kikuc May 19, 2024
87efcd5
Fix func name
t-kikuc May 22, 2024
79fe4c5
Rename struct from confusing one
t-kikuc May 22, 2024
faf376d
Refine order of args
t-kikuc May 22, 2024
152e82f
add test of hasOnlyPathsToSkip
t-kikuc May 22, 2024
2532f42
Refine testcase names
t-kikuc May 22, 2024
8329c41
rename 'expected' to 'skip'
t-kikuc May 22, 2024
a7c87b4
Fix testcases
t-kikuc May 22, 2024
f3fa841
separate func
t-kikuc May 22, 2024
41e136b
add test for skipByCommitMessagePrefixes
t-kikuc May 22, 2024
794e2e5
Moved package to new 'skipstage'
t-kikuc May 22, 2024
06641de
Add SkipOptions to ScriptRunStageOptions
t-kikuc May 22, 2024
fbb350f
rename 'SkipOptions' to 'SkipOn' to match with yaml
t-kikuc May 22, 2024
817830a
Move skip logic to scheduler
t-kikuc May 22, 2024
94618f9
Add missing reporting
t-kikuc May 22, 2024
e9caff2
Merge branch 'master' of https://github.com/pipe-cd/pipecd into skip-…
t-kikuc May 22, 2024
5b3585e
fix nits
t-kikuc May 22, 2024
5c46fe5
Fix check logic
t-kikuc May 22, 2024
2e4c9df
Add missing error handling
t-kikuc May 22, 2024
a687cd7
Move skipstage to controller
t-kikuc May 22, 2024
431682c
Update docs
t-kikuc May 22, 2024
3f0c007
gen mock by 'make gen/code'
t-kikuc May 22, 2024
d59b686
Merge branch 'master' of https://github.com/pipe-cd/pipecd into skip-…
t-kikuc May 29, 2024
b465c66
nits: fix from 'fromCmd' to 'byCmd'
t-kikuc May 30, 2024
674ea92
nits: rename func
t-kikuc May 30, 2024
ea2858a
nits: refine configuration-reference
t-kikuc May 30, 2024
fb4d46b
fix error logging of zap.Error(err)
t-kikuc May 30, 2024
c01b667
fix the flow in error handling
t-kikuc May 30, 2024
6fe1325
Fix: check skipping before creating executor
t-kikuc May 30, 2024
4e67ccb
Remove unused func 'GetCommitHashForRev'
t-kikuc May 30, 2024
e2e35f4
fix: remove unnecessary formatting
t-kikuc May 30, 2024
cab3539
refactor: quit separating 'skipstage' package
t-kikuc May 30, 2024
7eda335
fix: make 'checkSkipStage()' private after refactoring
t-kikuc May 30, 2024
b9e8a2a
Fix: rename test func
t-kikuc May 30, 2024
8a96a8c
Refactor: merge func 'checkSkipStage' into 'shouldSkipStage'
t-kikuc Jun 10, 2024
27e4acd
Merge funcs
t-kikuc Jun 10, 2024
38fae58
Add TestSkipByCommitMessagePrefixes with mock
t-kikuc Jun 10, 2024
0b074fb
Refactore: merge funcs and use mock
t-kikuc Jun 10, 2024
b63c1b1
Merge branch 'master' into skip-stage
t-kikuc Jun 10, 2024
cb6fd10
Merge branch 'master' into skip-stage
t-kikuc Jun 10, 2024
e498822
Merge branch 'master' into skip-stage
t-kikuc Jun 11, 2024
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
11 changes: 11 additions & 0 deletions docs/content/en/docs-dev/user-guide/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,13 @@ Note: The available values are identical to those found in the aws-sdk-go-v2 Typ
| Field | Type | Description | Required |
|-|-|-|-|

## SkipOptions

| Field | Type | Description | Required |
|-|-|-|-|
| commitMessagePrefixes | []string | List of commit message's prefixes. The stage will be skipped when the prefix of the commit's message matches any of them. Empty means the stage will not be skipped by this condition. | No |
| paths | []string | List of paths to directories or files. When all commit changes match them, the stage will be skipped. Empty means the stage will not be skipped by this condition. Regular expression can be used. | No |

## StageOptions

### KubernetesPrimaryRolloutStageOptions
Expand Down Expand Up @@ -705,12 +712,14 @@ Note: By default, the sum of traffic is rounded to 100. If both `primary` and `c
|-|-|-|-|
| duration | duration | Maximum time to perform the analysis. | Yes |
| metrics | [][AnalysisMetrics](#analysismetrics) | Configuration for analysis by metrics. | No |
| skipOn | [SkipOptions](#skipoptions) | When to skip this stage. | No |

### WaitStageOptions

| Field | Type | Description | Required |
|-|-|-|-|
| duration | duration | Time to wait. | Yes |
| skipOn | [SkipOptions](#skipoptions) | When to skip this stage. | No |

### WaitApprovalStageOptions

Expand All @@ -719,6 +728,7 @@ Note: By default, the sum of traffic is rounded to 100. If both `primary` and `c
| timeout | duration | The maximum length of time to wait before giving up. Default is 6h. | No |
| approvers | []string | List of username who has permission to approve. | Yes |
| minApproverNum | int | Number of minimum needed approvals to make this stage complete. Default is 1. | No |
| skipOn | [SkipOptions](#skipoptions) | When to skip this stage. | No |

### CustomSyncStageOptions (deprecated)
| Field | Type | Description | Required |
Expand All @@ -733,6 +743,7 @@ Note: By default, the sum of traffic is rounded to 100. If both `primary` and `c
| run | string | Script run on this stage. | Yes |
| env | map[string]string | Environment variables used with scripts. | No |
| timeout | duration | The maximum time the stage can be taken to run. Default is `6h`| No |
| skipOn | [SkipOptions](#skipoptions) | When to skip this stage. | No |

## PostSync

Expand Down
18 changes: 18 additions & 0 deletions pkg/app/piped/controller/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,24 @@
Notifier: s.notifier,
}

// Skip the stage if needed based on the skip config.
skip, err := s.shouldSkipStage(sig.Context(), input)
if err != nil {
lp.Errorf("failed to check whether skipping the stage: %w", err.Error())
if err := s.reportStageStatus(ctx, ps.Id, model.StageStatus_STAGE_FAILURE, ps.Requires); err != nil {
s.logger.Error("failed to report stage status", zap.Error(err))
}
return model.StageStatus_STAGE_FAILURE

Check warning on line 538 in pkg/app/piped/controller/scheduler.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/scheduler.go#L531-L538

Added lines #L531 - L538 were not covered by tests
}
if skip {
if err := s.reportStageStatus(ctx, ps.Id, model.StageStatus_STAGE_SKIPPED, ps.Requires); err != nil {
s.logger.Error("failed to report stage status", zap.Error(err))
return model.StageStatus_STAGE_FAILURE
}
lp.Info("The stage was successfully skipped due to the skip configuration of the stage.")
return model.StageStatus_STAGE_SKIPPED

Check warning on line 546 in pkg/app/piped/controller/scheduler.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/scheduler.go#L540-L546

Added lines #L540 - L546 were not covered by tests
}

// Find the executor for this stage.
ex, ok := executorFactory(input)
if !ok {
Expand Down
113 changes: 113 additions & 0 deletions pkg/app/piped/controller/skipstage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controller

import (
"context"
"strings"

"github.com/pipe-cd/pipecd/pkg/app/piped/executor"
"github.com/pipe-cd/pipecd/pkg/config"
"github.com/pipe-cd/pipecd/pkg/filematcher"
"github.com/pipe-cd/pipecd/pkg/git"
"github.com/pipe-cd/pipecd/pkg/model"
)

// checkSkipStage checks whether the stage should be skipped or not.
func (s *scheduler) shouldSkipStage(ctx context.Context, in executor.Input) (skip bool, err error) {
stageConfig := in.StageConfig
var skipOptions config.SkipOptions
switch stageConfig.Name {
case model.StageAnalysis:
skipOptions = stageConfig.AnalysisStageOptions.SkipOn
case model.StageWait:
skipOptions = stageConfig.WaitStageOptions.SkipOn
case model.StageWaitApproval:
skipOptions = stageConfig.WaitApprovalStageOptions.SkipOn
case model.StageScriptRun:
skipOptions = stageConfig.ScriptRunStageOptions.SkipOn
default:
return false, nil

Check warning on line 42 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L29-L42

Added lines #L29 - L42 were not covered by tests
}

if len(skipOptions.Paths) == 0 && len(skipOptions.CommitMessagePrefixes) == 0 {
// When no condition is specified.
return false, nil
}

Check warning on line 48 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L45-L48

Added lines #L45 - L48 were not covered by tests

repoCfg := in.Application.GitPath.Repo
repo, err := in.GitClient.Clone(ctx, repoCfg.Id, repoCfg.Remote, repoCfg.Branch, "")
if err != nil {
return false, err
}

Check warning on line 54 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L50-L54

Added lines #L50 - L54 were not covered by tests

// Check by path pattern
skip, err = skipByPathPattern(ctx, skipOptions, repo, in.RunningDSP.Revision(), in.TargetDSP.Revision())
if err != nil {
return false, err
}
if skip {
return true, nil
}

Check warning on line 63 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L57-L63

Added lines #L57 - L63 were not covered by tests

// Check by prefix of commit message
skip, err = skipByCommitMessagePrefixes(ctx, skipOptions, repo, in.TargetDSP.Revision())
return skip, err

Check warning on line 67 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L66-L67

Added lines #L66 - L67 were not covered by tests
}

// skipByCommitMessagePrefixes returns true if the commit message has ANY one of the specified prefixes.
func skipByCommitMessagePrefixes(ctx context.Context, opt config.SkipOptions, repo git.Repo, targetRev string) (skip bool, err error) {
if len(opt.CommitMessagePrefixes) == 0 {
return false, nil
}

commit, err := repo.GetCommitForRev(ctx, targetRev)
if err != nil {
return false, err
}

Check warning on line 79 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L78-L79

Added lines #L78 - L79 were not covered by tests

for _, prefix := range opt.CommitMessagePrefixes {
if strings.HasPrefix(commit.Message, prefix) {
return true, nil
}
}
return false, nil
}

// skipByPathPattern returns true if and only if ALL changed files are included in `opt.Paths`.
// If ANY changed file does not match all `skipPatterns`, it returns false.
func skipByPathPattern(ctx context.Context, opt config.SkipOptions, repo git.Repo, runningRev, targetRev string) (skip bool, err error) {
if len(opt.Paths) == 0 {
return false, nil
}

changedFiles, err := repo.ChangedFiles(ctx, runningRev, targetRev)
if err != nil {
return false, err
}

Check warning on line 99 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L98-L99

Added lines #L98 - L99 were not covered by tests

matcher, err := filematcher.NewPatternMatcher(opt.Paths)
if err != nil {
return false, err
}

Check warning on line 104 in pkg/app/piped/controller/skipstage.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/controller/skipstage.go#L103-L104

Added lines #L103 - L104 were not covered by tests

for _, changedFile := range changedFiles {
if !matcher.Matches(changedFile) {
return false, nil
}
}

return true, nil
}
157 changes: 157 additions & 0 deletions pkg/app/piped/controller/skipstage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controller

import (
"context"
"testing"

"github.com/golang/mock/gomock"
"github.com/pipe-cd/pipecd/pkg/config"
"github.com/pipe-cd/pipecd/pkg/git"
"github.com/pipe-cd/pipecd/pkg/git/gittest"
"github.com/stretchr/testify/assert"
)

func TestSkipByCommitMessagePrefixes(t *testing.T) {
t.Parallel()
testcases := []struct {
name string
commitMessage string
prefixes []string
skip bool
}{
{
name: "no prefixes",
commitMessage: "test message",
prefixes: []string{},
skip: false,
},
{
name: "no commit message",
commitMessage: "",
prefixes: []string{"to-skip"},
skip: false,
},
{
name: "prefix matches",
commitMessage: "to-skip: test message",
prefixes: []string{"to-skip"},
skip: true,
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repoMock := gittest.NewMockRepo(ctrl)
repoMock.EXPECT().GetCommitForRev(gomock.Any(), gomock.Any()).Return(git.Commit{
Message: tc.commitMessage,
}, nil).AnyTimes()

opt := config.SkipOptions{
CommitMessagePrefixes: tc.prefixes,
}
skip, err := skipByCommitMessagePrefixes(context.Background(), opt, repoMock, "test-rev")
assert.Equal(t, tc.skip, skip)
assert.NoError(t, err)
})
}
}

func TestSkipByPathPattern(t *testing.T) {
t.Parallel()
testcases := []struct {
name string
skipPatterns []string
changedFiles []string
skip bool
}{
{
name: "no skip patterns",
skipPatterns: nil,
changedFiles: []string{"file1"},
skip: false,
},
{
name: "no changed files",
skipPatterns: []string{"file1"},
changedFiles: nil,
skip: true,
},
{
name: "no skip patterns and no changed files",
skipPatterns: nil,
changedFiles: nil,
skip: false,
},
{
name: "skip pattern matches all changed files",
skipPatterns: []string{"file1", "file2"},
changedFiles: []string{"file1", "file2"},
skip: true,
},
{
name: "skip pattern does not match changed files",
skipPatterns: []string{"file1", "file2"},
changedFiles: []string{"file1", "file3"},
skip: false,
},
{
name: "skip files of a directory",
skipPatterns: []string{"dir1/*"},
changedFiles: []string{"dir1/file1", "dir1/file2"},
skip: true,
},
{
name: "skip files recursively",
skipPatterns: []string{"dir1/**"},
changedFiles: []string{"dir1/file1", "dir1/sub/file2"},
skip: true,
},
{
name: "skip files with the extension recursively",
skipPatterns: []string{"dir1/**/*.yaml"},
changedFiles: []string{"dir1/file1.yaml", "dir1/sub1/file2.yaml", "dir1/sub1/sub2/file3.yaml"},
skip: true,
},
{
name: "skip files not recursively",
skipPatterns: []string{"*.yaml"},
changedFiles: []string{"dir1/file1.yaml"},
skip: false,
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// We do not use t.Parallel() here due to https://pkg.go.dev/github.com/pipe-cd/pipecd/pkg/filematcher#PatternMatcher.Matches.
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repoMock := gittest.NewMockRepo(ctrl)
repoMock.EXPECT().ChangedFiles(gomock.Any(), gomock.Any(), gomock.Any()).Return(tc.changedFiles, nil).AnyTimes()

opt := config.SkipOptions{
Paths: tc.skipPatterns,
}
actual, err := skipByPathPattern(context.Background(), opt, repoMock, "running-rev", "target-rev")
assert.NoError(t, err)
assert.Equal(t, tc.skip, actual)
})
}
}
4 changes: 2 additions & 2 deletions pkg/app/piped/executor/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
for {
select {
case <-ticker.C:
if !e.checkSkipped(ctx) {
if !e.checkSkippedByCmd(ctx) {

Check warning on line 115 in pkg/app/piped/executor/analysis/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/executor/analysis/analysis.go#L115

Added line #L115 was not covered by tests
continue
}
status = model.StageStatus_STAGE_SKIPPED
Expand Down Expand Up @@ -339,7 +339,7 @@
return args
}

func (e *Executor) checkSkipped(ctx context.Context) bool {
func (e *Executor) checkSkippedByCmd(ctx context.Context) bool {

Check warning on line 342 in pkg/app/piped/executor/analysis/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/piped/executor/analysis/analysis.go#L342

Added line #L342 was not covered by tests
var skipCmd *model.ReportableCommand
commands := e.CommandLister.ListCommands()

Expand Down
Loading
Loading