diff --git a/cmd/server.go b/cmd/server.go index b0405768f3..b4da1b42c1 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -73,6 +73,7 @@ const ( SilenceForkPRErrorsFlag = "silence-fork-pr-errors" SilenceVCSStatusNoPlans = "silence-vcs-status-no-plans" SilenceWhitelistErrorsFlag = "silence-whitelist-errors" + SkipCloneNoChanges = "skip-clone-no-changes" SlackTokenFlag = "slack-token" SSLCertFileFlag = "ssl-cert-file" SSLKeyFileFlag = "ssl-key-file" @@ -289,6 +290,10 @@ var boolFlags = map[string]boolFlag{ " This writes secrets to disk and should only be enabled in a secure environment.", defaultValue: false, }, + SkipCloneNoChanges: { + description: "Skips cloning the PR repo if there are no projects were changed in the PR.", + defaultValue: false, + }, } var intFlags = map[string]intFlag{ PortFlag: { diff --git a/cmd/server_test.go b/cmd/server_test.go index 7a197b1e77..807b89afa1 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -82,6 +82,7 @@ var testFlags = map[string]interface{}{ SilenceForkPRErrorsFlag: true, SilenceWhitelistErrorsFlag: true, SilenceVCSStatusNoPlans: true, + SkipCloneNoChanges: true, SlackTokenFlag: "slack-token", SSLCertFileFlag: "cert-file", SSLKeyFileFlag: "key-file", diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index f88085da43..451710865e 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -56,6 +56,7 @@ type DefaultProjectCommandBuilder struct { GlobalCfg valid.GlobalCfg PendingPlanFinder *DefaultPendingPlanFinder CommentBuilder CommentBuilder + SkipCloneNoChanges bool } // See ProjectCommandBuilder.BuildAutoplanCommands. @@ -96,39 +97,64 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c // buildPlanAllCommands builds plan contexts for all projects we determine were // modified in this ctx. func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { - // Need to lock the workspace we're about to clone to. - workspace := DefaultWorkspace - unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, workspace) - if err != nil { - ctx.Log.Warn("workspace was locked") - return nil, err - } - ctx.Log.Debug("got workspace lock") - defer unlockFn() - - // We'll need the list of modified files. - modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull) - if err != nil { - return nil, err - } - ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles)) - - repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace) - if err != nil { - return nil, err - } - - // Parse config file if it exists. - hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir) - if err != nil { - return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir) - } + // We'll need the list of modified files. + modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull) + + if err != nil { + return nil, err + } + ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles)) + + if p.SkipCloneNoChanges && p.VCSClient.IsSupportDownloadSingleFile(ctx.BaseRepo) { + hasRepoCfg, repoCfgData, err := p.VCSClient.DownloadRepoConfigFile(ctx.Pull) + if err != nil { + return nil, errors.Wrapf(err, "downloading %s", yaml.AtlantisYAMLFilename) + } + + if hasRepoCfg { + repoCfg, err := p.ParserValidator.ParseRepoCfg(repoCfgData, "", p.GlobalCfg, ctx.BaseRepo.ID()) + if err != nil { + return nil, errors.Wrapf(err, "parsing %s", yaml.AtlantisYAMLFilename) + } + ctx.Log.Info("successfully parsed remote %s file", yaml.AtlantisYAMLFilename) + matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "") + if err != nil { + return nil, err + } + ctx.Log.Info("%d projects are changed on MR %q based on their when_modified config", len(matchingProjects), ctx.Pull.Num) + if len(matchingProjects) == 0 { + ctx.Log.Info("skipping repo clone since no project was modified") + return []models.ProjectCommandContext{}, nil + } + } + } + + // Need to lock the workspace we're about to clone to. + workspace := DefaultWorkspace + + unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, workspace) + if err != nil { + ctx.Log.Warn("workspace was locked") + return nil, err + } + ctx.Log.Debug("got workspace lock") + defer unlockFn() + + repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace) + if err != nil { + return nil, err + } + // Parse config file if it exists. + hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir) + if err != nil { + return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir) + } var projCtxs []models.ProjectCommandContext if hasRepoCfg { // If there's a repo cfg then we'll use it to figure out which projects // should be planed. - repoCfg, err := p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.BaseRepo.ID()) + repoCfg, err := p.ParserValidator.ParseRepoCfg([]byte{}, repoDir, p.GlobalCfg, ctx.BaseRepo.ID()) if err != nil { return nil, errors.Wrapf(err, "parsing %s", yaml.AtlantisYAMLFilename) } @@ -159,6 +185,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, return projCtxs, nil } + // buildProjectPlanCommand builds a plan context for a single project. // cmd must be for only one project. func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) { @@ -306,7 +333,7 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName s } var repoConfig valid.RepoCfg - repoConfig, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.BaseRepo.ID()) + repoConfig, err = p.ParserValidator.ParseRepoCfg([]byte{}, repoDir, p.GlobalCfg, ctx.BaseRepo.ID()) if err != nil { return } diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index eb020ec3a6..5de7bba5ca 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -571,6 +571,7 @@ projects: PendingPlanFinder: &DefaultPendingPlanFinder{}, CommentBuilder: &CommentParser{}, GlobalCfg: globalCfg, + SkipCloneNoChanges: false, } // We run a test for each type of command. diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index e87ce20ad7..790a10a536 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -143,6 +143,7 @@ projects: PendingPlanFinder: &events.DefaultPendingPlanFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(false, false, false), + SkipCloneNoChanges: false, } ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{ @@ -365,6 +366,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(true, false, false), + SkipCloneNoChanges: false, } var actCtxs []models.ProjectCommandContext @@ -498,6 +500,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(true, false, false), + SkipCloneNoChanges: false, } ctxs, err := builder.BuildPlanCommands( @@ -570,6 +573,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { PendingPlanFinder: &events.DefaultPendingPlanFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(false, false, false), + SkipCloneNoChanges: false, } ctxs, err := builder.BuildApplyCommands( @@ -637,6 +641,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(true, false, false), + SkipCloneNoChanges: false, } ctx := &events.CommandContext{ @@ -699,6 +704,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { ProjectFinder: &events.DefaultProjectFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(true, false, false), + SkipCloneNoChanges: false, } var actCtxs []models.ProjectCommandContext @@ -863,6 +869,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, CommentBuilder: &events.CommentParser{}, GlobalCfg: valid.NewGlobalCfg(true, false, false), + SkipCloneNoChanges: false, } actCtxs, err := builder.BuildPlanCommands( diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index 1e75f8f9c7..743e60edf6 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -364,3 +364,11 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st } return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:] } + +func (g *AzureDevopsClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return false +} + +func (g *AzureDevopsClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return false, []byte{}, fmt.Errorf("Not Implemented") +} diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index b8bcb8d16a..e5c53f79be 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -243,3 +243,11 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } return respBody, nil } + +func (b *Client) IsSupportDownloadSingleFile( models.Repo) bool { + return false +} + +func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return false, []byte{}, fmt.Errorf("Not Implemented") +} diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index 10653439e4..239486a8fc 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -311,3 +311,11 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } return respBody, nil } + +func (b *Client) IsSupportDownloadSingleFile(repo models.Repo) bool { + return false +} + +func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return false, []byte{}, fmt.Errorf("not implemented") +} diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index 6cd35b39a4..589bf49112 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -38,4 +38,7 @@ type Client interface { UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error MergePull(pull models.PullRequest) error MarkdownPullLink(pull models.PullRequest) (string, error) + + DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) + IsSupportDownloadSingleFile(repo models.Repo) bool } diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index ce0370b0f4..ea5986278d 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -16,8 +16,11 @@ package vcs import ( "context" "fmt" - "net/url" + "github.com/runatlantis/atlantis/server/events/yaml" + "net/http" + "net/url" "strings" + "encoding/base64" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs/common" @@ -324,3 +327,28 @@ func (g *GithubClient) MergePull(pull models.PullRequest) error { func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) { return fmt.Sprintf("#%d", pull.Num), nil } + + +func (g *GithubClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + opt := github.RepositoryContentGetOptions{Ref: pull.BaseBranch} + fileContent, _, resp, err := g.client.Repositories.GetContents(g.ctx, pull.BaseRepo.Owner, pull.BaseRepo.Name, yaml.AtlantisYAMLFilename, &opt) + + if resp.StatusCode == http.StatusNotFound { + return false, []byte{}, nil + } + if err != nil { + return true, []byte{}, err + } + + decodedData, err := base64.StdEncoding.DecodeString(*fileContent.Content) + if err != nil { + return true, []byte{}, err + } + + return true, decodedData, nil +} + +func (g *GithubClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return true +} + diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index b29388813c..436d56b23c 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -14,19 +14,21 @@ package vcs import ( - "fmt" - "net" - "net/url" - "strings" + "fmt" + "github.com/runatlantis/atlantis/server/events/yaml" + "net" + "net/http" + "net/url" + "strings" - "github.com/runatlantis/atlantis/server/events/vcs/common" + "github.com/runatlantis/atlantis/server/events/vcs/common" - version "github.com/hashicorp/go-version" - "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/logging" + version "github.com/hashicorp/go-version" + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/logging" - "github.com/runatlantis/atlantis/server/events/models" - gitlab "github.com/xanzy/go-gitlab" + "github.com/runatlantis/atlantis/server/events/models" + gitlab "github.com/xanzy/go-gitlab" ) type GitlabClient struct { @@ -263,3 +265,23 @@ func MustConstraint(constraint string) version.Constraints { } return c } + + +func (g *GitlabClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + opt := gitlab.GetRawFileOptions{Ref: gitlab.String(pull.BaseBranch)} + + bytes, resp, err := g.Client.RepositoryFiles.GetRawFile(pull.BaseRepo.ID(), yaml.AtlantisYAMLFilename, &opt) + if resp.StatusCode == http.StatusNotFound { + return false, []byte{}, nil + } + + if err != nil { + return true, []byte{}, err + } + + return true, bytes, nil +} + +func (g *GitlabClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return true +} diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 62e022f1f3..7c94bb840e 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -161,6 +161,14 @@ func (mock *MockClient) MarkdownPullLink(pull models.PullRequest) (string, error return ret0, ret1 } +func (mock *MockClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return false +} + +func (mock *MockClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return false, []byte{}, nil +} + func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient { return &VerifierMockClient{ mock: mock, diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 6a5b3d747b..d8e2b80fb3 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -53,3 +53,11 @@ func (a *NotConfiguredVCSClient) MarkdownPullLink(pull models.PullRequest) (stri func (a *NotConfiguredVCSClient) err() error { return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String()) } + +func (a *NotConfiguredVCSClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return false +} + +func (a *NotConfiguredVCSClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return true, []byte{}, a.err() +} diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index ed25c4d60d..fe6e98b061 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -83,3 +83,11 @@ func (d *ClientProxy) MergePull(pull models.PullRequest) error { func (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) { return d.clients[pull.BaseRepo.VCSHost.Type].MarkdownPullLink(pull) } + +func (d *ClientProxy) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return d.clients[pull.BaseRepo.VCSHost.Type].DownloadRepoConfigFile(pull) +} + +func (d *ClientProxy) IsSupportDownloadSingleFile(repo models.Repo) bool { + return d.clients[repo.VCSHost.Type].IsSupportDownloadSingleFile(repo) +} diff --git a/server/events/yaml/parser_validator.go b/server/events/yaml/parser_validator.go index 0e2bd84a24..bc31e8b9f0 100644 --- a/server/events/yaml/parser_validator.go +++ b/server/events/yaml/parser_validator.go @@ -44,18 +44,25 @@ func (p *ParserValidator) HasRepoCfg(absRepoDir string) (bool, error) { // ParseRepoCfg returns the parsed and validated atlantis.yaml config for the // repo at absRepoDir. // If there was no config file, it will return an os.IsNotExist(error). -func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.GlobalCfg, repoID string) (valid.RepoCfg, error) { - configFile := p.repoCfgPath(absRepoDir, AtlantisYAMLFilename) - configData, err := ioutil.ReadFile(configFile) // nolint: gosec - - if err != nil { - if !os.IsNotExist(err) { - return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename) - } - // Don't wrap os.IsNotExist errors because we want our callers to be - // able to detect if it's a NotExist err. - return valid.RepoCfg{}, err - } +func (p *ParserValidator) ParseRepoCfg(repoCfgData []byte, absRepoDir string, globalCfg valid.GlobalCfg, repoID string) (valid.RepoCfg, error) { + var configData []byte + var err error + + if len(repoCfgData) > 0 { + configData = repoCfgData + } else { + configFile := p.repoCfgPath(absRepoDir, AtlantisYAMLFilename) + configData, err = ioutil.ReadFile(configFile) // nolint: gosec + + if err != nil { + if !os.IsNotExist(err) { + return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename) + } + // Don't wrap os.IsNotExist errors because we want our callers to be + // able to detect if it's a NotExist err. + return valid.RepoCfg{}, err + } + } var rawConfig raw.RepoCfg if err := yaml.UnmarshalStrict(configData, &rawConfig); err != nil { diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index fd687317b8..cb28e0c95f 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -46,7 +46,7 @@ func TestHasRepoCfg_InvalidFileExtension(t *testing.T) { func TestParseRepoCfg_DirDoesNotExist(t *testing.T) { r := yaml.ParserValidator{} - _, err := r.ParseRepoCfg("/not/exist", globalCfg, "") + _, err := r.ParseRepoCfg([]byte{},"/not/exist", globalCfg, "") Assert(t, os.IsNotExist(err), "exp not exist err") } @@ -54,7 +54,7 @@ func TestParseRepoCfg_FileDoesNotExist(t *testing.T) { tmpDir, cleanup := TempDir(t) defer cleanup() r := yaml.ParserValidator{} - _, err := r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err := r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") Assert(t, os.IsNotExist(err), "exp not exist err") } @@ -65,7 +65,7 @@ func TestParseRepoCfg_BadPermissions(t *testing.T) { Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") ErrContains(t, "unable to read atlantis.yaml file: ", err) } @@ -99,7 +99,7 @@ func TestParseCfgs_InvalidYAML(t *testing.T) { err := ioutil.WriteFile(confPath, []byte(c.input), 0600) Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") ErrContains(t, c.expErr, err) _, err = r.ParseGlobalCfg(confPath, valid.NewGlobalCfg(false, false, false)) ErrContains(t, c.expErr, err) @@ -845,7 +845,7 @@ workflows: Ok(t, err) r := yaml.ParserValidator{} - act, err := r.ParseRepoCfg(tmpDir, globalCfg, "") + act, err := r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") if c.expErr != "" { ErrEquals(t, c.expErr, err) return @@ -873,7 +873,7 @@ workflows: Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, valid.NewGlobalCfg(false, false, false), "repo_id") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, valid.NewGlobalCfg(false, false, false), "repo_id") ErrEquals(t, "repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'", err) } @@ -1337,7 +1337,7 @@ func TestParseRepoCfg_V2ShellParsing(t *testing.T) { Ok(t, ioutil.WriteFile(v3Path, []byte("version: 3\n"+cfg), 0600)) p := &yaml.ParserValidator{} - v2Cfg, err := p.ParseRepoCfg(v2Dir, valid.NewGlobalCfg(true, false, false), "") + v2Cfg, err := p.ParseRepoCfg([]byte{}, v2Dir, valid.NewGlobalCfg(true, false, false), "") if c.expV2Err != "" { ErrEquals(t, c.expV2Err, err) } else { @@ -1346,7 +1346,7 @@ func TestParseRepoCfg_V2ShellParsing(t *testing.T) { Equals(t, c.expV2, v2Cfg.Workflows["custom"].Apply.Steps[0].RunCommand) } - v3Cfg, err := p.ParseRepoCfg(v3Dir, valid.NewGlobalCfg(true, false, false), "") + v3Cfg, err := p.ParseRepoCfg([]byte{}, v3Dir, valid.NewGlobalCfg(true, false, false), "") Ok(t, err) Equals(t, c.in, v3Cfg.Workflows["custom"].Plan.Steps[0].RunCommand) Equals(t, c.in, v3Cfg.Workflows["custom"].Apply.Steps[0].RunCommand) diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index be4fda14d8..cff01f5037 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -465,6 +465,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. PendingPlanFinder: &events.DefaultPendingPlanFinder{}, CommentBuilder: commentParser, GlobalCfg: globalCfg, + SkipCloneNoChanges: false, }, DB: boltdb, PendingPlanFinder: &events.DefaultPendingPlanFinder{}, diff --git a/server/server.go b/server/server.go index 831bf44622..81a11305f7 100644 --- a/server/server.go +++ b/server/server.go @@ -331,6 +331,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GlobalCfg: globalCfg, PendingPlanFinder: pendingPlanFinder, CommentBuilder: commentParser, + SkipCloneNoChanges: userConfig.SkipCloneNoChanges, }, ProjectCommandRunner: &events.DefaultProjectCommandRunner{ Locker: projectLocker, diff --git a/server/user_config.go b/server/user_config.go index 2524636707..d7fd9d0888 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -49,6 +49,7 @@ type UserConfig struct { // are found. SilenceVCSStatusNoPlans bool `mapstructure:"silence-vcs-status-no-plans"` SilenceWhitelistErrors bool `mapstructure:"silence-whitelist-errors"` + SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` SlackToken string `mapstructure:"slack-token"` SSLCertFile string `mapstructure:"ssl-cert-file"` SSLKeyFile string `mapstructure:"ssl-key-file"`