-
Notifications
You must be signed in to change notification settings - Fork 156
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
Changes from 40 commits
06c5f5d
022e940
59b14ab
691db8a
7c37add
ced6054
3809d14
7d7f9b7
c4a529d
e925ef7
ee89a23
87efcd5
79fe4c5
faf376d
152e82f
2532f42
8329c41
a7c87b4
f3fa841
41e136b
794e2e5
06641de
fbb350f
817830a
94618f9
e9caff2
5b3585e
5c46fe5
2e4c9df
a687cd7
431682c
3f0c007
d59b686
b465c66
674ea92
ea2858a
fb4d46b
c01b667
6fe1325
4e67ccb
e2e35f4
cab3539
7eda335
b9e8a2a
8a96a8c
27e4acd
38fae58
0b074fb
b63c1b1
cb6fd10
e498822
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import ( | |
"go.uber.org/zap" | ||
|
||
"github.com/pipe-cd/pipecd/pkg/app/piped/controller/controllermetrics" | ||
"github.com/pipe-cd/pipecd/pkg/app/piped/controller/skipstage" | ||
"github.com/pipe-cd/pipecd/pkg/app/piped/deploysource" | ||
"github.com/pipe-cd/pipecd/pkg/app/piped/executor" | ||
"github.com/pipe-cd/pipecd/pkg/app/piped/executor/registry" | ||
|
@@ -528,6 +529,24 @@ func (s *scheduler) executeStage(sig executor.StopSignal, ps model.PipelineStage | |
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 | ||
} | ||
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.Infof("The stage was successfully skipped due to the skip configuration of the stage.") | ||
return model.StageStatus_STAGE_SKIPPED | ||
} | ||
|
||
// Find the executor for this stage. | ||
ex, ok := executorFactory(input) | ||
if !ok { | ||
|
@@ -687,6 +706,25 @@ func (s *scheduler) reportDeploymentCompleted(ctx context.Context, status model. | |
return err | ||
} | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [imo] How about adding the func There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the current one is enough because
WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I got it 👍 |
||
} | ||
|
||
return skipstage.CheckSkipStage(ctx, in, skipOptions) | ||
} | ||
|
||
func (s *scheduler) getMentionedAccounts(event model.NotificationEventType) ([]string, error) { | ||
n, ok := s.metadataStore.Shared().Get(model.MetadataKeyDeploymentNotification) | ||
if !ok { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// 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 skipstage | ||
|
||
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" | ||
) | ||
|
||
// CheckSkipStage checks whether the stage should be skipped or not. | ||
func CheckSkipStage(ctx context.Context, in executor.Input, opt config.SkipOptions) (skip bool, err error) { | ||
if len(opt.Paths) == 0 && len(opt.CommitMessagePrefixes) == 0 { | ||
// When no condition is specified. | ||
return false, nil | ||
} | ||
|
||
repoCfg := in.Application.GitPath.Repo | ||
repo, err := in.GitClient.Clone(ctx, repoCfg.Id, repoCfg.Remote, repoCfg.Branch, "") | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
// Check by path pattern | ||
skip, err = skipByPathPattern(ctx, opt, repo, in.RunningDSP.Revision(), in.TargetDSP.Revision()) | ||
if err != nil { | ||
return false, err | ||
} | ||
if skip { | ||
return true, nil | ||
} | ||
|
||
// Check by prefix of commit message | ||
skip, err = skipByCommitMessagePrefixes(ctx, opt, repo, in.TargetDSP.Revision()) | ||
return skip, err | ||
} | ||
|
||
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 | ||
} | ||
|
||
return commitMessageHasAnyPrefix(commit.Message, opt.CommitMessagePrefixes), nil | ||
} | ||
|
||
func commitMessageHasAnyPrefix(commitMessage string, prefixes []string) bool { | ||
for _, prefix := range prefixes { | ||
if strings.HasPrefix(commitMessage, prefix) { | ||
return true | ||
} | ||
} | ||
return 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 | ||
} | ||
return hasOnlyPathsToSkip(opt.Paths, changedFiles) | ||
} | ||
|
||
// hasOnlyPathsToSkip returns true if and only if all changed files are included in `skipPatterns`. | ||
// If any changed file does not match all `skipPatterns`, it returns false. | ||
func hasOnlyPathsToSkip(skipPatterns []string, changedFiles []string) (bool, error) { | ||
matcher, err := filematcher.NewPatternMatcher(skipPatterns) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
for _, changedFile := range changedFiles { | ||
if !matcher.Matches(changedFile) { | ||
return false, nil | ||
} | ||
} | ||
|
||
return true, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// 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 skipstage | ||
|
||
import ( | ||
"testing" | ||
|
||
"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) { | ||
skip := commitMessageHasAnyPrefix(tc.commitMessage, tc.prefixes) | ||
assert.Equal(t, tc.skip, skip) | ||
}) | ||
} | ||
} | ||
|
||
func TestHasOnlyPathsToSkip(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: true, | ||
}, | ||
{ | ||
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. | ||
actual, err := hasOnlyPathsToSkip(tc.skipPatterns, tc.changedFiles) | ||
assert.NoError(t, err) | ||
assert.Equal(t, tc.skip, actual) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits. There is no need to use formater here 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I fixed it now. e2e35f4